3 '''automated testing library for testing Samba against windows'''
5 import pexpect, subprocess
7 import sys, os, time, re
10 '''testing of Samba against windows VMs'''
14 self.list_mode = False
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:
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))
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="))
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'''
54 if len(line) == 0 or line[0] == '#':
56 colon = line.find(':')
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'''
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'''
74 self.vms = vms.split(',')
77 '''return True if we should skip a step'''
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.
87 if isinstance(text, list):
89 for i in range(len(ret)):
90 ret[i] = self.substitute(ret[i])
93 """We may have objects such as pexpect.EOF that are not strings"""
94 if not isinstance(text, str):
97 var_start = text.find("${")
100 var_end = text.find("}", var_start)
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])
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'):
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'''
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))
140 def run_cmd(self, cmd, dir=".", show=None, output=False, checkfail=True):
142 cmd = self.substitute(cmd)
143 if isinstance(cmd, list):
144 self.info('$ ' + " ".join(cmd))
146 self.info('$ ' + cmd)
148 return subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=dir).communicate()[0]
149 if isinstance(cmd, list):
154 return subprocess.check_call(cmd, shell=shell, cwd=dir)
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'''
162 cmd = self.substitute(cmd)
163 if isinstance(cmd, list):
164 self.info('$ ' + " ".join(cmd))
166 self.info('$ ' + cmd)
167 if isinstance(cmd, list):
172 ret = subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT)
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,
183 '''check that command output contains the listed strings'''
185 if isinstance(contains, str):
186 contains = [contains]
188 out = self.cmd_output(cmd)
190 for c in self.substitute(contains):
195 m = re.search(c, out)
203 start = out.upper().find(c.upper())
210 raise RuntimeError("Expected to not see %s in %s" % (c, cmd))
213 raise RuntimeError("Expected to see %s in %s" % (c, cmd))
214 if ordered and start != -1:
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'''
222 self.cmd_contains(cmd, contains, nomatch=wait_for_fail,
223 ordered=ordered, regex=regex, casefold=casefold)
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)
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)
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):
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
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'])
266 child.expect('your original resolv.conf')
267 child.expect('nameserver')
268 child.expect('\d+.\d+.\d+.\d+')
271 def vm_poweroff(self, vmname, checkfail=True):
273 self.setvar('VMNAME', vmname)
274 self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
276 def vm_reset(self, vmname):
278 self.setvar('VMNAME', vmname)
279 self.run_cmd("${VM_RESET}")
281 def vm_restore(self, vmname, snapshot):
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)
293 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
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")
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"])
321 child.sendline(time.strftime("%d-%m-%y", time_tuple))
323 child.sendline(time.strftime("%m-%d-%y", time_tuple))
325 child.sendline("time")
326 child.expect("Enter the new time:")
327 child.sendline(time.strftime("%H:%M:%S", time_tuple))
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)
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",
353 "passed test Replications"])
358 child.sendline("net config Workstation")
359 child.expect("Workstation domain")
360 child.expect('[\S]+')
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'''
366 if domain.upper() == self.getvar("WIN_DOMAIN").upper():
369 child.expect('[\S]+')
370 hostname = child.after
371 if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
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")
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")
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"])
392 child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
393 i = child.expect(["Ok", "The following command was not found"])
395 self.info("Firewall disable failed - ignoring")
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)
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)
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)
433 def resolve_ip(self, hostname, retries=60, delay=5):
434 '''resolve an IP given a hostname, assuming NBT'''
436 child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
437 i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
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'''
451 if self.getvar('WIN_IP'):
452 ip = self.getvar('WIN_IP')
454 ip = self.resolve_ip(hostname)
455 self.setvar('WIN_IP', ip)
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",
463 "Connection refused",
469 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
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",
478 "Connection refused",
484 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
488 if self.set_dns(child):
491 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
495 self.run_date_time(child, None)
498 self.run_tlntadmn(child)
501 self.set_noexpire(child, username)
504 self.disable_firewall(child)
505 disable_firewall = False
508 if self.set_ip(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('@')
523 username = '@'.join(s)
524 child = self.pexpect_spawn('kinit ' + username)
525 child.expect("Password")
526 child.sendline(password)
527 child.expect(pexpect.EOF)
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'''
536 if v[-6:] == "_REALM":
538 if base + '_IP' in self.vars:
539 ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
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
552 self.port_wait("${WIN_IP}", 139)
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"))
561 '''return a dictionary of all the configured VM names'''
565 ret.append(self.vars[v])
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")
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)
595 self.list_steps_mode()
598 self.setvar('PREFIX', self.opts.prefix)
600 if self.opts.sourcetree:
601 self.setvar('SOURCETREE', self.opts.sourcetree)
604 self.info('rebasing')
605 self.chdir('${SOURCETREE}')
606 self.run_cmd('git pull --rebase')
609 self.info('cleaning')
610 self.chdir('${SOURCETREE}/' + subdir)
611 self.run_cmd('make clean')