s4: Implement UDP echo server example
[Samba.git] / wintest / wintest.py
blob0d65116562a369629d432ce2a7f6295b59aa4135
1 #!/usr/bin/env python
3 '''automated testing library for testing Samba against windows'''
5 import pexpect, subprocess
6 import optparse
7 import sys, os, time, re
9 class wintest():
10 '''testing of Samba against windows VMs'''
12 def __init__(self):
13 self.vars = {}
14 self.list_mode = False
15 self.vms = None
16 os.putenv('PYTHONUNBUFFERED', '1')
17 self.parser = optparse.OptionParser("wintest")
19 def setvar(self, varname, value):
20 '''set a substitution variable'''
21 self.vars[varname] = value
23 def getvar(self, varname):
24 '''return a substitution variable'''
25 if not varname in self.vars:
26 return None
27 return self.vars[varname]
29 def setwinvars(self, vm, prefix='WIN'):
30 '''setup WIN_XX vars based on a vm name'''
31 for v in ['VM', 'HOSTNAME', 'USER', 'PASS', 'SNAPSHOT', 'REALM', 'DOMAIN', 'IP']:
32 vname = '%s_%s' % (vm, v)
33 if vname in self.vars:
34 self.setvar("%s_%s" % (prefix,v), self.substitute("${%s}" % vname))
35 else:
36 self.vars.pop("%s_%s" % (prefix,v), None)
38 if self.getvar("WIN_REALM"):
39 self.setvar("WIN_REALM", self.getvar("WIN_REALM").upper())
40 self.setvar("WIN_LCREALM", self.getvar("WIN_REALM").lower())
41 dnsdomain = self.getvar("WIN_REALM")
42 self.setvar("WIN_BASEDN", "DC=" + dnsdomain.replace(".", ",DC="))
44 def info(self, msg):
45 '''print some information'''
46 if not self.list_mode:
47 print(self.substitute(msg))
49 def load_config(self, fname):
50 '''load the config file'''
51 f = open(fname)
52 for line in f:
53 line = line.strip()
54 if len(line) == 0 or line[0] == '#':
55 continue
56 colon = line.find(':')
57 if colon == -1:
58 raise RuntimeError("Invalid config line '%s'" % line)
59 varname = line[0:colon].strip()
60 value = line[colon+1:].strip()
61 self.setvar(varname, value)
63 def list_steps_mode(self):
64 '''put wintest in step listing mode'''
65 self.list_mode = True
67 def set_skip(self, skiplist):
68 '''set a list of tests to skip'''
69 self.skiplist = skiplist.split(',')
71 def set_vms(self, vms):
72 '''set a list of VMs to test'''
73 if vms is not None:
74 self.vms = vms.split(',')
76 def skip(self, step):
77 '''return True if we should skip a step'''
78 if self.list_mode:
79 print("\t%s" % step)
80 return True
81 return step in self.skiplist
83 def substitute(self, text):
84 """Substitute strings of the form ${NAME} in text, replacing
85 with substitutions from vars.
86 """
87 if isinstance(text, list):
88 ret = text[:]
89 for i in range(len(ret)):
90 ret[i] = self.substitute(ret[i])
91 return ret
93 """We may have objects such as pexpect.EOF that are not strings"""
94 if not isinstance(text, str):
95 return text
96 while True:
97 var_start = text.find("${")
98 if var_start == -1:
99 return text
100 var_end = text.find("}", var_start)
101 if var_end == -1:
102 return text
103 var_name = text[var_start+2:var_end]
104 if not var_name in self.vars:
105 raise RuntimeError("Unknown substitution variable ${%s}" % var_name)
106 text = text.replace("${%s}" % var_name, self.vars[var_name])
107 return text
109 def have_var(self, varname):
110 '''see if a variable has been set'''
111 return varname in self.vars
113 def have_vm(self, vmname):
114 '''see if a VM should be used'''
115 if not self.have_var(vmname + '_VM'):
116 return False
117 if self.vms is None:
118 return True
119 return vmname in self.vms
121 def putenv(self, key, value):
122 '''putenv with substitution'''
123 os.putenv(key, self.substitute(value))
125 def chdir(self, dir):
126 '''chdir with substitution'''
127 os.chdir(self.substitute(dir))
129 def del_files(self, dirs):
130 '''delete all files in the given directory'''
131 for d in dirs:
132 self.run_cmd("find %s -type f | xargs rm -f" % d)
134 def write_file(self, filename, text, mode='w'):
135 '''write to a file'''
136 f = open(self.substitute(filename), mode=mode)
137 f.write(self.substitute(text))
138 f.close()
140 def run_cmd(self, cmd, dir=".", show=None, output=False, checkfail=True):
141 '''run a command'''
142 cmd = self.substitute(cmd)
143 if isinstance(cmd, list):
144 self.info('$ ' + " ".join(cmd))
145 else:
146 self.info('$ ' + cmd)
147 if output:
148 return subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=dir).communicate()[0]
149 if isinstance(cmd, list):
150 shell=False
151 else:
152 shell=True
153 if checkfail:
154 return subprocess.check_call(cmd, shell=shell, cwd=dir)
155 else:
156 return subprocess.call(cmd, shell=shell, cwd=dir)
159 def run_child(self, cmd, dir="."):
160 '''create a child and return the Popen handle to it'''
161 cwd = os.getcwd()
162 cmd = self.substitute(cmd)
163 if isinstance(cmd, list):
164 self.info('$ ' + " ".join(cmd))
165 else:
166 self.info('$ ' + cmd)
167 if isinstance(cmd, list):
168 shell=False
169 else:
170 shell=True
171 os.chdir(dir)
172 ret = subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT)
173 os.chdir(cwd)
174 return ret
176 def cmd_output(self, cmd):
177 '''return output from and command'''
178 cmd = self.substitute(cmd)
179 return self.run_cmd(cmd, output=True)
181 def cmd_contains(self, cmd, contains, nomatch=False, ordered=False, regex=False,
182 casefold=True):
183 '''check that command output contains the listed strings'''
185 if isinstance(contains, str):
186 contains = [contains]
188 out = self.cmd_output(cmd)
189 self.info(out)
190 for c in self.substitute(contains):
191 if regex:
192 if casefold:
193 c = c.upper()
194 out = out.upper()
195 m = re.search(c, out)
196 if m is None:
197 start = -1
198 end = -1
199 else:
200 start = m.start()
201 end = m.end()
202 elif casefold:
203 start = out.upper().find(c.upper())
204 end = start + len(c)
205 else:
206 start = out.find(c)
207 end = start + len(c)
208 if nomatch:
209 if start != -1:
210 raise RuntimeError("Expected to not see %s in %s" % (c, cmd))
211 else:
212 if start == -1:
213 raise RuntimeError("Expected to see %s in %s" % (c, cmd))
214 if ordered and start != -1:
215 out = out[end:]
217 def retry_cmd(self, cmd, contains, retries=30, delay=2, wait_for_fail=False,
218 ordered=False, regex=False, casefold=True):
219 '''retry a command a number of times'''
220 while retries > 0:
221 try:
222 self.cmd_contains(cmd, contains, nomatch=wait_for_fail,
223 ordered=ordered, regex=regex, casefold=casefold)
224 return
225 except:
226 time.sleep(delay)
227 retries -= 1
228 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
229 raise RuntimeError("Failed to find %s" % contains)
231 def pexpect_spawn(self, cmd, timeout=60, crlf=True, casefold=True):
232 '''wrapper around pexpect spawn'''
233 cmd = self.substitute(cmd)
234 self.info("$ " + cmd)
235 ret = pexpect.spawn(cmd, logfile=sys.stdout, timeout=timeout)
237 def sendline_sub(line):
238 line = self.substitute(line)
239 if crlf:
240 line = line.replace('\n', '\r\n') + '\r'
241 return ret.old_sendline(line)
243 def expect_sub(line, timeout=ret.timeout, casefold=casefold):
244 line = self.substitute(line)
245 if casefold:
246 if isinstance(line, list):
247 for i in range(len(line)):
248 if isinstance(line[i], str):
249 line[i] = '(?i)' + line[i]
250 elif isinstance(line, str):
251 line = '(?i)' + line
252 return ret.old_expect(line, timeout=timeout)
254 ret.old_sendline = ret.sendline
255 ret.sendline = sendline_sub
256 ret.old_expect = ret.expect
257 ret.expect = expect_sub
259 return ret
261 def get_nameserver(self):
262 '''Get the current nameserver from /etc/resolv.conf'''
263 child = self.pexpect_spawn('cat /etc/resolv.conf', crlf=False)
264 i = child.expect(['Generated by wintest', 'nameserver'])
265 if i == 0:
266 child.expect('your original resolv.conf')
267 child.expect('nameserver')
268 child.expect('\d+.\d+.\d+.\d+')
269 return child.after
271 def vm_poweroff(self, vmname, checkfail=True):
272 '''power off a VM'''
273 self.setvar('VMNAME', vmname)
274 self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
276 def vm_reset(self, vmname):
277 '''reset a VM'''
278 self.setvar('VMNAME', vmname)
279 self.run_cmd("${VM_RESET}")
281 def vm_restore(self, vmname, snapshot):
282 '''restore a VM'''
283 self.setvar('VMNAME', vmname)
284 self.setvar('SNAPSHOT', snapshot)
285 self.run_cmd("${VM_RESTORE}")
287 def ping_wait(self, hostname):
288 '''wait for a hostname to come up on the network'''
289 hostname = self.substitute(hostname)
290 loops=10
291 while loops > 0:
292 try:
293 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
294 break
295 except:
296 loops = loops - 1
297 if loops == 0:
298 raise RuntimeError("Failed to ping %s" % hostname)
299 self.info("Host %s is up" % hostname)
301 def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
302 '''wait for a host to come up on the network'''
303 self.retry_cmd("nc -v -z -w 1 %s %u" % (hostname, port), ['succeeded'],
304 retries=retries, delay=delay, wait_for_fail=wait_for_fail)
306 def run_net_time(self, child):
307 '''run net time on windows'''
308 child.sendline("net time \\\\${HOSTNAME} /set")
309 child.expect("Do you want to set the local computer")
310 child.sendline("Y")
311 child.expect("The command completed successfully")
313 def run_date_time(self, child, time_tuple=None):
314 '''run date and time on windows'''
315 if time_tuple is None:
316 time_tuple = time.localtime()
317 child.sendline("date")
318 child.expect("Enter the new date:")
319 i = child.expect(["dd-mm-yy", "mm-dd-yy"])
320 if i == 0:
321 child.sendline(time.strftime("%d-%m-%y", time_tuple))
322 else:
323 child.sendline(time.strftime("%m-%d-%y", time_tuple))
324 child.expect("C:")
325 child.sendline("time")
326 child.expect("Enter the new time:")
327 child.sendline(time.strftime("%H:%M:%S", time_tuple))
328 child.expect("C:")
330 def get_ipconfig(self, child):
331 '''get the IP configuration of the child'''
332 child.sendline("ipconfig /all")
333 child.expect('Ethernet adapter ')
334 child.expect("[\w\s]+")
335 self.setvar("WIN_NIC", child.after)
336 child.expect(['IPv4 Address', 'IP Address'])
337 child.expect('\d+.\d+.\d+.\d+')
338 self.setvar('WIN_IPV4_ADDRESS', child.after)
339 child.expect('Subnet Mask')
340 child.expect('\d+.\d+.\d+.\d+')
341 self.setvar('WIN_SUBNET_MASK', child.after)
342 child.expect('Default Gateway')
343 child.expect('\d+.\d+.\d+.\d+')
344 self.setvar('WIN_DEFAULT_GATEWAY', child.after)
345 child.expect("C:")
347 def get_is_dc(self, child):
348 '''check if a windows machine is a domain controller'''
349 child.sendline("dcdiag")
350 i = child.expect(["is not a Directory Server",
351 "is not recognized as an internal or external command",
352 "Home Server = ",
353 "passed test Replications"])
354 if i == 0:
355 return False
356 if i == 1 or i == 3:
357 child.expect("C:")
358 child.sendline("net config Workstation")
359 child.expect("Workstation domain")
360 child.expect('[\S]+')
361 domain = child.after
362 i = child.expect(["Workstation Domain DNS Name", "Logon domain"])
363 '''If we get the Logon domain first, we are not in an AD domain'''
364 if i == 1:
365 return False
366 if domain.upper() == self.getvar("WIN_DOMAIN").upper():
367 return True
369 child.expect('[\S]+')
370 hostname = child.after
371 if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
372 return True
374 def set_noexpire(self, child, username):
375 '''Ensure this user's password does not expire'''
376 child.sendline('wmic useraccount where name="%s" set PasswordExpires=FALSE' % username)
377 child.expect("update successful")
378 child.expect("C:")
380 def run_tlntadmn(self, child):
381 '''remove the annoying telnet restrictions'''
382 child.sendline('tlntadmn config maxconn=1024')
383 child.expect("The settings were successfully updated")
384 child.expect("C:")
386 def disable_firewall(self, child):
387 '''remove the annoying firewall'''
388 child.sendline('netsh advfirewall set allprofiles state off')
389 i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
390 child.expect("C:")
391 if i == 1:
392 child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
393 i = child.expect(["Ok", "The following command was not found"])
394 if i != 0:
395 self.info("Firewall disable failed - ignoring")
396 child.expect("C:")
398 def set_dns(self, child):
399 child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
400 i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
401 if i > 0:
402 return True
403 else:
404 return False
406 def set_ip(self, child):
407 """fix the IP address to the same value it had when we
408 connected, but don't use DHCP, and force the DNS server to our
409 DNS server. This allows DNS updates to run"""
410 self.get_ipconfig(child)
411 if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
412 raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
413 self.getvar("WIN_IP")))
414 child.sendline('netsh')
415 child.expect('netsh>')
416 child.sendline('offline')
417 child.expect('netsh>')
418 child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
419 child.expect('netsh>')
420 child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
421 i = child.expect(['The syntax supplied for this command is not valid. Check help for the correct syntax', 'netsh>', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
422 if i == 0:
423 child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
424 child.expect('netsh>')
425 child.sendline('commit')
426 child.sendline('online')
427 child.sendline('exit')
429 child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
430 return True
433 def resolve_ip(self, hostname, retries=60, delay=5):
434 '''resolve an IP given a hostname, assuming NBT'''
435 while retries > 0:
436 child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
437 i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
438 if i == 0:
439 return child.after
440 retries -= 1
441 time.sleep(delay)
442 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
443 raise RuntimeError("Failed to resolve IP of %s" % hostname)
446 def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
447 disable_firewall=True, run_tlntadmn=True, set_noexpire=False):
448 '''open a telnet connection to a windows server, return the pexpect child'''
449 set_route = False
450 set_dns = False
451 if self.getvar('WIN_IP'):
452 ip = self.getvar('WIN_IP')
453 else:
454 ip = self.resolve_ip(hostname)
455 self.setvar('WIN_IP', ip)
456 while retries > 0:
457 child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
458 i = child.expect(["Welcome to Microsoft Telnet Service",
459 "Denying new connections due to the limit on number of connections",
460 "No more connections are allowed to telnet server",
461 "Unable to connect to remote host",
462 "No route to host",
463 "Connection refused",
464 pexpect.EOF])
465 if i != 0:
466 child.close()
467 time.sleep(delay)
468 retries -= 1
469 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
470 continue
471 child.expect("password:")
472 child.sendline(password)
473 i = child.expect(["C:",
474 "Denying new connections due to the limit on number of connections",
475 "No more connections are allowed to telnet server",
476 "Unable to connect to remote host",
477 "No route to host",
478 "Connection refused",
479 pexpect.EOF])
480 if i != 0:
481 child.close()
482 time.sleep(delay)
483 retries -= 1
484 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
485 continue
486 if set_dns:
487 set_dns = False
488 if self.set_dns(child):
489 continue;
490 if set_route:
491 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
492 child.expect("C:")
493 set_route = False
494 if set_time:
495 self.run_date_time(child, None)
496 set_time = False
497 if run_tlntadmn:
498 self.run_tlntadmn(child)
499 run_tlntadmn = False
500 if set_noexpire:
501 self.set_noexpire(child, username)
502 set_noexpire = False
503 if disable_firewall:
504 self.disable_firewall(child)
505 disable_firewall = False
506 if set_ip:
507 set_ip = False
508 if self.set_ip(child):
509 set_route = True
510 set_dns = True
511 continue
512 return child
513 raise RuntimeError("Failed to connect with telnet")
515 def kinit(self, username, password):
516 '''use kinit to setup a credentials cache'''
517 self.run_cmd("kdestroy")
518 self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
519 username = self.substitute(username)
520 s = username.split('@')
521 if len(s) > 0:
522 s[1] = s[1].upper()
523 username = '@'.join(s)
524 child = self.pexpect_spawn('kinit ' + username)
525 child.expect("Password")
526 child.sendline(password)
527 child.expect(pexpect.EOF)
528 child.close()
529 if child.exitstatus != 0:
530 raise RuntimeError("kinit failed with status %d" % child.exitstatus)
532 def get_domains(self):
533 '''return a dictionary of DNS domains and IPs for named.conf'''
534 ret = {}
535 for v in self.vars:
536 if v[-6:] == "_REALM":
537 base = v[:-6]
538 if base + '_IP' in self.vars:
539 ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
540 return ret
542 def wait_reboot(self, retries=3):
543 '''wait for a VM to reboot'''
545 # first wait for it to shutdown
546 self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
548 # now wait for it to come back. If it fails to come back
549 # then try resetting it
550 while retries > 0:
551 try:
552 self.port_wait("${WIN_IP}", 139)
553 return
554 except:
555 retries -= 1
556 self.vm_reset("${WIN_VM}")
557 self.info("retrying reboot (retries=%u)" % retries)
558 raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
560 def get_vms(self):
561 '''return a dictionary of all the configured VM names'''
562 ret = []
563 for v in self.vars:
564 if v[-3:] == "_VM":
565 ret.append(self.vars[v])
566 return ret
568 def setup(self, testname, subdir):
569 '''setup for main tests, parsing command line'''
570 self.parser.add_option("--conf", type='string', default='', help='config file')
571 self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
572 self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
573 self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
574 self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
575 self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
576 self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
577 self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
578 self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
580 self.opts, self.args = self.parser.parse_args()
582 if not self.opts.conf:
583 print("Please specify a config file with --conf")
584 sys.exit(1)
586 # we don't need fsync safety in these tests
587 self.putenv('TDB_NO_FSYNC', '1')
589 self.load_config(self.opts.conf)
591 self.set_skip(self.opts.skip)
592 self.set_vms(self.opts.vms)
594 if self.opts.list:
595 self.list_steps_mode()
597 if self.opts.prefix:
598 self.setvar('PREFIX', self.opts.prefix)
600 if self.opts.sourcetree:
601 self.setvar('SOURCETREE', self.opts.sourcetree)
603 if self.opts.rebase:
604 self.info('rebasing')
605 self.chdir('${SOURCETREE}')
606 self.run_cmd('git pull --rebase')
608 if self.opts.clean:
609 self.info('cleaning')
610 self.chdir('${SOURCETREE}/' + subdir)
611 self.run_cmd('make clean')