s4:kdc: Implement KDC plugin hardware authentication policy
[Samba.git] / wintest / wintest.py
blob7d59e7adb386fc66b72e087600646c11f7ba15f3
1 #!/usr/bin/env python3
3 '''automated testing library for testing Samba against windows'''
5 import pexpect
6 import subprocess
7 import optparse
8 import sys
9 import os
10 import time
11 import re
14 class wintest():
15 '''testing of Samba against windows VMs'''
17 def __init__(self):
18 self.vars = {}
19 self.list_mode = False
20 self.vms = None
21 os.environ['PYTHONUNBUFFERED'] = '1'
22 self.parser = optparse.OptionParser("wintest")
24 def check_prerequesites(self):
25 self.info("Checking prerequisites")
26 self.setvar('HOSTNAME', self.cmd_output("hostname -s").strip())
27 if os.getuid() != 0:
28 raise Exception("You must run this script as root")
29 self.run_cmd('ifconfig ${INTERFACE} ${INTERFACE_NET} up')
30 if self.getvar('INTERFACE_IPV6'):
31 self.run_cmd('ifconfig ${INTERFACE} inet6 del ${INTERFACE_IPV6}/64', checkfail=False)
32 self.run_cmd('ifconfig ${INTERFACE} inet6 add ${INTERFACE_IPV6}/64 up')
34 self.run_cmd('ifconfig ${NAMED_INTERFACE} ${NAMED_INTERFACE_NET} up')
35 if self.getvar('NAMED_INTERFACE_IPV6'):
36 self.run_cmd('ifconfig ${NAMED_INTERFACE} inet6 del ${NAMED_INTERFACE_IPV6}/64', checkfail=False)
37 self.run_cmd('ifconfig ${NAMED_INTERFACE} inet6 add ${NAMED_INTERFACE_IPV6}/64 up')
39 def stop_vms(self):
40 '''Shut down any existing alive VMs, so they do not collide with what we are doing'''
41 self.info('Shutting down any of our VMs already running')
42 vms = self.get_vms()
43 for v in vms:
44 self.vm_poweroff(v, checkfail=False)
46 def setvar(self, varname, value):
47 '''set a substitution variable'''
48 self.vars[varname] = value
50 def getvar(self, varname):
51 '''return a substitution variable'''
52 if varname not in self.vars:
53 return None
54 return self.vars[varname]
56 def setwinvars(self, vm, prefix='WIN'):
57 '''setup WIN_XX vars based on a vm name'''
58 for v in ['VM', 'HOSTNAME', 'USER', 'PASS', 'SNAPSHOT', 'REALM', 'DOMAIN', 'IP']:
59 vname = '%s_%s' % (vm, v)
60 if vname in self.vars:
61 self.setvar("%s_%s" % (prefix, v), self.substitute("${%s}" % vname))
62 else:
63 self.vars.pop("%s_%s" % (prefix, v), None)
65 if self.getvar("WIN_REALM"):
66 self.setvar("WIN_REALM", self.getvar("WIN_REALM").upper())
67 self.setvar("WIN_LCREALM", self.getvar("WIN_REALM").lower())
68 dnsdomain = self.getvar("WIN_REALM")
69 self.setvar("WIN_BASEDN", "DC=" + dnsdomain.replace(".", ",DC="))
70 if self.getvar("WIN_USER") is None:
71 self.setvar("WIN_USER", "administrator")
73 def info(self, msg):
74 '''print some information'''
75 if not self.list_mode:
76 print(self.substitute(msg))
78 def load_config(self, fname):
79 '''load the config file'''
80 f = open(fname)
81 for line in f:
82 line = line.strip()
83 if len(line) == 0 or line[0] == '#':
84 continue
85 colon = line.find(':')
86 if colon == -1:
87 raise RuntimeError("Invalid config line '%s'" % line)
88 varname = line[0:colon].strip()
89 value = line[colon + 1:].strip()
90 self.setvar(varname, value)
92 def list_steps_mode(self):
93 '''put wintest in step listing mode'''
94 self.list_mode = True
96 def set_skip(self, skiplist):
97 '''set a list of tests to skip'''
98 self.skiplist = skiplist.split(',')
100 def set_vms(self, vms):
101 '''set a list of VMs to test'''
102 if vms is not None:
103 self.vms = []
104 for vm in vms.split(','):
105 vm = vm.upper()
106 self.vms.append(vm)
108 def skip(self, step):
109 '''return True if we should skip a step'''
110 if self.list_mode:
111 print("\t%s" % step)
112 return True
113 return step in self.skiplist
115 def substitute(self, text):
116 """Substitute strings of the form ${NAME} in text, replacing
117 with substitutions from vars.
119 if isinstance(text, list):
120 ret = text[:]
121 for i in range(len(ret)):
122 ret[i] = self.substitute(ret[i])
123 return ret
125 """We may have objects such as pexpect.EOF that are not strings"""
126 if not isinstance(text, str):
127 return text
128 while True:
129 var_start = text.find("${")
130 if var_start == -1:
131 return text
132 var_end = text.find("}", var_start)
133 if var_end == -1:
134 return text
135 var_name = text[var_start + 2:var_end]
136 if var_name not in self.vars:
137 raise RuntimeError("Unknown substitution variable ${%s}" % var_name)
138 text = text.replace("${%s}" % var_name, self.vars[var_name])
140 def have_var(self, varname):
141 '''see if a variable has been set'''
142 return varname in self.vars
144 def have_vm(self, vmname):
145 '''see if a VM should be used'''
146 if not self.have_var(vmname + '_VM'):
147 return False
148 if self.vms is None:
149 return True
150 return vmname in self.vms
152 def putenv(self, key, value):
153 '''putenv with substitution'''
154 os.environ[key] = self.substitute(value)
156 def chdir(self, dir):
157 '''chdir with substitution'''
158 os.chdir(self.substitute(dir))
160 def del_files(self, dirs):
161 '''delete all files in the given directory'''
162 for d in dirs:
163 self.run_cmd("find %s -type f | xargs rm -f" % d)
165 def write_file(self, filename, text, mode='w'):
166 '''write to a file'''
167 f = open(self.substitute(filename), mode=mode)
168 f.write(self.substitute(text))
169 f.close()
171 def run_cmd(self, cmd, dir=".", show=None, output=False, checkfail=True):
172 '''run a command'''
173 cmd = self.substitute(cmd)
174 if isinstance(cmd, list):
175 self.info('$ ' + " ".join(cmd))
176 else:
177 self.info('$ ' + cmd)
178 if output:
179 return subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=dir).communicate()[0]
180 if isinstance(cmd, list):
181 shell = False
182 else:
183 shell = True
184 if checkfail:
185 return subprocess.check_call(cmd, shell=shell, cwd=dir)
186 else:
187 return subprocess.call(cmd, shell=shell, cwd=dir)
189 def run_child(self, cmd, dir="."):
190 '''create a child and return the Popen handle to it'''
191 cwd = os.getcwd()
192 cmd = self.substitute(cmd)
193 if isinstance(cmd, list):
194 self.info('$ ' + " ".join(cmd))
195 else:
196 self.info('$ ' + cmd)
197 if isinstance(cmd, list):
198 shell = False
199 else:
200 shell = True
201 os.chdir(dir)
202 ret = subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT)
203 os.chdir(cwd)
204 return ret
206 def cmd_output(self, cmd):
207 '''return output from and command'''
208 cmd = self.substitute(cmd)
209 return self.run_cmd(cmd, output=True)
211 def cmd_contains(self, cmd, contains, nomatch=False, ordered=False, regex=False,
212 casefold=True):
213 '''check that command output contains the listed strings'''
215 if isinstance(contains, str):
216 contains = [contains]
218 out = self.cmd_output(cmd)
219 self.info(out)
220 for c in self.substitute(contains):
221 if regex:
222 if casefold:
223 c = c.upper()
224 out = out.upper()
225 m = re.search(c, out)
226 if m is None:
227 start = -1
228 end = -1
229 else:
230 start = m.start()
231 end = m.end()
232 elif casefold:
233 start = out.upper().find(c.upper())
234 end = start + len(c)
235 else:
236 start = out.find(c)
237 end = start + len(c)
238 if nomatch:
239 if start != -1:
240 raise RuntimeError("Expected to not see %s in %s" % (c, cmd))
241 else:
242 if start == -1:
243 raise RuntimeError("Expected to see %s in %s" % (c, cmd))
244 if ordered and start != -1:
245 out = out[end:]
247 def retry_cmd(self, cmd, contains, retries=30, delay=2, wait_for_fail=False,
248 ordered=False, regex=False, casefold=True):
249 '''retry a command a number of times'''
250 while retries > 0:
251 try:
252 self.cmd_contains(cmd, contains, nomatch=wait_for_fail,
253 ordered=ordered, regex=regex, casefold=casefold)
254 return
255 except:
256 time.sleep(delay)
257 retries -= 1
258 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
259 raise RuntimeError("Failed to find %s" % contains)
261 def pexpect_spawn(self, cmd, timeout=60, crlf=True, casefold=True):
262 '''wrapper around pexpect spawn'''
263 cmd = self.substitute(cmd)
264 self.info("$ " + cmd)
265 ret = pexpect.spawn(cmd, logfile=sys.stdout, timeout=timeout)
267 def sendline_sub(line):
268 line = self.substitute(line)
269 if crlf:
270 line = line.replace('\n', '\r\n') + '\r'
271 return ret.old_sendline(line)
273 def expect_sub(line, timeout=ret.timeout, casefold=casefold):
274 line = self.substitute(line)
275 if casefold:
276 if isinstance(line, list):
277 for i in range(len(line)):
278 if isinstance(line[i], str):
279 line[i] = '(?i)' + line[i]
280 elif isinstance(line, str):
281 line = '(?i)' + line
282 return ret.old_expect(line, timeout=timeout)
284 ret.old_sendline = ret.sendline
285 ret.sendline = sendline_sub
286 ret.old_expect = ret.expect
287 ret.expect = expect_sub
289 return ret
291 def get_nameserver(self):
292 '''Get the current nameserver from /etc/resolv.conf'''
293 child = self.pexpect_spawn('cat /etc/resolv.conf', crlf=False)
294 i = child.expect(['Generated by wintest', 'nameserver'])
295 if i == 0:
296 child.expect('your original resolv.conf')
297 child.expect('nameserver')
298 child.expect(r'\d+.\d+.\d+.\d+')
299 return child.after
301 def rndc_cmd(self, cmd, checkfail=True):
302 '''run a rndc command'''
303 self.run_cmd("${RNDC} -c ${PREFIX}/etc/rndc.conf %s" % cmd, checkfail=checkfail)
305 def named_supports_gssapi_keytab(self):
306 '''see if named supports tkey-gssapi-keytab'''
307 self.write_file("${PREFIX}/named.conf.test",
308 'options { tkey-gssapi-keytab "test"; };')
309 try:
310 self.run_cmd("${NAMED_CHECKCONF} ${PREFIX}/named.conf.test")
311 except subprocess.CalledProcessError:
312 return False
313 return True
315 def set_nameserver(self, nameserver):
316 '''set the nameserver in resolv.conf'''
317 self.write_file("/etc/resolv.conf.wintest", '''
318 # Generated by wintest, the Samba v Windows automated testing system
319 nameserver %s
321 # your original resolv.conf appears below:
322 ''' % self.substitute(nameserver))
323 child = self.pexpect_spawn("cat /etc/resolv.conf", crlf=False)
324 i = child.expect(['your original resolv.conf appears below:', pexpect.EOF])
325 if i == 0:
326 child.expect(pexpect.EOF)
327 contents = child.before.lstrip().replace('\r', '')
328 self.write_file('/etc/resolv.conf.wintest', contents, mode='a')
329 self.write_file('/etc/resolv.conf.wintest-bak', contents)
330 self.run_cmd("mv -f /etc/resolv.conf.wintest /etc/resolv.conf")
331 self.resolv_conf_backup = '/etc/resolv.conf.wintest-bak'
333 def configure_bind(self, kerberos_support=False, include=None):
334 self.chdir('${PREFIX}')
336 if self.getvar('NAMED_INTERFACE_IPV6'):
337 ipv6_listen = 'listen-on-v6 port 53 { ${NAMED_INTERFACE_IPV6}; };'
338 else:
339 ipv6_listen = ''
340 self.setvar('BIND_LISTEN_IPV6', ipv6_listen)
342 if not kerberos_support:
343 self.setvar("NAMED_TKEY_OPTION", "")
344 elif self.getvar('NAMESERVER_BACKEND') != 'SAMBA_INTERNAL':
345 if self.named_supports_gssapi_keytab():
346 self.setvar("NAMED_TKEY_OPTION",
347 'tkey-gssapi-keytab "${PREFIX}/bind-dns/dns.keytab";')
348 else:
349 self.info("LCREALM=${LCREALM}")
350 self.setvar("NAMED_TKEY_OPTION",
351 '''tkey-gssapi-credential "DNS/${LCREALM}";
352 tkey-domain "${LCREALM}";
353 ''')
354 self.putenv('KEYTAB_FILE', '${PREFIX}/bind-dns/dns.keytab')
355 self.putenv('KRB5_KTNAME', '${PREFIX}/bind-dns/dns.keytab')
356 else:
357 self.setvar("NAMED_TKEY_OPTION", "")
359 if include and self.getvar('NAMESERVER_BACKEND') != 'SAMBA_INTERNAL':
360 self.setvar("NAMED_INCLUDE", 'include "%s";' % include)
361 else:
362 self.setvar("NAMED_INCLUDE", '')
364 self.run_cmd("mkdir -p ${PREFIX}/etc")
366 self.write_file("etc/named.conf", '''
367 options {
368 listen-on port 53 { ${NAMED_INTERFACE_IP}; };
369 ${BIND_LISTEN_IPV6}
370 directory "${PREFIX}/var/named";
371 dump-file "${PREFIX}/var/named/data/cache_dump.db";
372 pid-file "${PREFIX}/var/named/named.pid";
373 statistics-file "${PREFIX}/var/named/data/named_stats.txt";
374 memstatistics-file "${PREFIX}/var/named/data/named_mem_stats.txt";
375 allow-query { any; };
376 recursion yes;
377 ${NAMED_TKEY_OPTION}
378 max-cache-ttl 10;
379 max-ncache-ttl 10;
381 forward only;
382 forwarders {
383 ${DNSSERVER};
388 key "rndc-key" {
389 algorithm hmac-md5;
390 secret "lA/cTrno03mt5Ju17ybEYw==";
393 controls {
394 inet ${NAMED_INTERFACE_IP} port 953
395 allow { any; } keys { "rndc-key"; };
398 ${NAMED_INCLUDE}
399 ''')
401 if self.getvar('NAMESERVER_BACKEND') == 'SAMBA_INTERNAL':
402 self.write_file('etc/named.conf',
404 zone "%s" IN {
405 type forward;
406 forward only;
407 forwarders {
411 ''' % (self.getvar('LCREALM'), self.getvar('INTERFACE_IP')),
412 mode='a')
414 # add forwarding for the windows domains
415 domains = self.get_domains()
417 for d in domains:
418 self.write_file('etc/named.conf',
420 zone "%s" IN {
421 type forward;
422 forward only;
423 forwarders {
427 ''' % (d, domains[d]),
428 mode='a')
430 self.write_file("etc/rndc.conf", '''
431 # Start of rndc.conf
432 key "rndc-key" {
433 algorithm hmac-md5;
434 secret "lA/cTrno03mt5Ju17ybEYw==";
437 options {
438 default-key "rndc-key";
439 default-server ${NAMED_INTERFACE_IP};
440 default-port 953;
442 ''')
444 def stop_bind(self):
445 '''Stop our private BIND from listening and operating'''
446 self.rndc_cmd("stop", checkfail=False)
447 self.port_wait("${NAMED_INTERFACE_IP}", 53, wait_for_fail=True)
449 self.run_cmd("rm -rf var/named")
451 def start_bind(self):
452 '''restart the test environment version of bind'''
453 self.info("Restarting bind9")
454 self.chdir('${PREFIX}')
456 self.set_nameserver(self.getvar('NAMED_INTERFACE_IP'))
458 self.run_cmd("mkdir -p var/named/data")
459 self.run_cmd("chown -R ${BIND_USER} var/named")
461 self.bind_child = self.run_child("${BIND9} -u ${BIND_USER} -n 1 -c ${PREFIX}/etc/named.conf -g")
463 self.port_wait("${NAMED_INTERFACE_IP}", 53)
464 self.rndc_cmd("flush")
466 def restart_bind(self, kerberos_support=False, include=None):
467 self.configure_bind(kerberos_support=kerberos_support, include=include)
468 self.stop_bind()
469 self.start_bind()
471 def restore_resolv_conf(self):
472 '''restore the /etc/resolv.conf after testing is complete'''
473 if getattr(self, 'resolv_conf_backup', False):
474 self.info("restoring /etc/resolv.conf")
475 self.run_cmd("mv -f %s /etc/resolv.conf" % self.resolv_conf_backup)
477 def vm_poweroff(self, vmname, checkfail=True):
478 '''power off a VM'''
479 self.setvar('VMNAME', vmname)
480 self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
482 def vm_reset(self, vmname):
483 '''reset a VM'''
484 self.setvar('VMNAME', vmname)
485 self.run_cmd("${VM_RESET}")
487 def vm_restore(self, vmname, snapshot):
488 '''restore a VM'''
489 self.setvar('VMNAME', vmname)
490 self.setvar('SNAPSHOT', snapshot)
491 self.run_cmd("${VM_RESTORE}")
493 def ping_wait(self, hostname):
494 '''wait for a hostname to come up on the network'''
495 hostname = self.substitute(hostname)
496 loops = 10
497 while loops > 0:
498 try:
499 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
500 break
501 except:
502 loops = loops - 1
503 if loops == 0:
504 raise RuntimeError("Failed to ping %s" % hostname)
505 self.info("Host %s is up" % hostname)
507 def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
508 '''wait for a host to come up on the network'''
510 while retries > 0:
511 child = self.pexpect_spawn("nc -v -z -w 1 %s %u" % (hostname, port), crlf=False, timeout=1)
512 child.expect([pexpect.EOF, pexpect.TIMEOUT])
513 child.close()
514 i = child.exitstatus
515 if wait_for_fail:
516 # wait for timeout or fail
517 if i is None or i > 0:
518 return
519 else:
520 if i == 0:
521 return
523 time.sleep(delay)
524 retries -= 1
525 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
527 raise RuntimeError("gave up waiting for %s:%d" % (hostname, port))
529 def run_net_time(self, child):
530 '''run net time on windows'''
531 child.sendline("net time \\\\${HOSTNAME} /set")
532 child.expect("Do you want to set the local computer")
533 child.sendline("Y")
534 child.expect("The command completed successfully")
536 def run_date_time(self, child, time_tuple=None):
537 '''run date and time on windows'''
538 if time_tuple is None:
539 time_tuple = time.localtime()
540 child.sendline("date")
541 child.expect("Enter the new date:")
542 i = child.expect(["dd-mm-yy", "mm-dd-yy"])
543 if i == 0:
544 child.sendline(time.strftime("%d-%m-%y", time_tuple))
545 else:
546 child.sendline(time.strftime("%m-%d-%y", time_tuple))
547 child.expect("C:")
548 child.sendline("time")
549 child.expect("Enter the new time:")
550 child.sendline(time.strftime("%H:%M:%S", time_tuple))
551 child.expect("C:")
553 def get_ipconfig(self, child):
554 '''get the IP configuration of the child'''
555 child.sendline("ipconfig /all")
556 child.expect('Ethernet adapter ')
557 child.expect(r"[\w\s]+")
558 self.setvar("WIN_NIC", child.after)
559 child.expect(['IPv4 Address', 'IP Address'])
560 child.expect(r'\d+.\d+.\d+.\d+')
561 self.setvar('WIN_IPV4_ADDRESS', child.after)
562 child.expect('Subnet Mask')
563 child.expect(r'\d+.\d+.\d+.\d+')
564 self.setvar('WIN_SUBNET_MASK', child.after)
565 child.expect('Default Gateway')
566 i = child.expect([r'\d+.\d+.\d+.\d+', "C:"])
567 if i == 0:
568 self.setvar('WIN_DEFAULT_GATEWAY', child.after)
569 child.expect("C:")
571 def get_is_dc(self, child):
572 '''check if a windows machine is a domain controller'''
573 child.sendline("dcdiag")
574 i = child.expect(["is not a [Directory Server|DC]",
575 "is not recognized as an internal or external command",
576 "Home Server = ",
577 "passed test Replications"])
578 if i == 0:
579 return False
580 if i == 1 or i == 3:
581 child.expect("C:")
582 child.sendline("net config Workstation")
583 child.expect("Workstation domain")
584 child.expect(r'[\S]+')
585 domain = child.after
586 i = child.expect(["Workstation Domain DNS Name", "Logon domain"])
587 '''If we get the Logon domain first, we are not in an AD domain'''
588 if i == 1:
589 return False
590 if domain.upper() == self.getvar("WIN_DOMAIN").upper():
591 return True
593 child.expect(r'[\S]+')
594 hostname = child.after
595 if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
596 return True
598 def set_noexpire(self, child, username):
599 """Ensure this user's password does not expire"""
600 child.sendline('wmic useraccount where name="%s" set PasswordExpires=FALSE' % username)
601 child.expect("update successful")
602 child.expect("C:")
604 def run_tlntadmn(self, child):
605 '''remove the annoying telnet restrictions'''
606 child.sendline('tlntadmn config maxconn=1024')
607 child.expect(["The settings were successfully updated", "Access is denied"])
608 child.expect("C:")
610 def disable_firewall(self, child):
611 '''remove the annoying firewall'''
612 child.sendline('netsh advfirewall set allprofiles state off')
613 i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off", "The requested operation requires elevation", "Access is denied"])
614 child.expect("C:")
615 if i == 1:
616 child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
617 i = child.expect(["Ok", "The following command was not found", "Access is denied"])
618 if i != 0:
619 self.info("Firewall disable failed - ignoring")
620 child.expect("C:")
622 def set_dns(self, child):
623 child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${NAMED_INTERFACE_IP} primary')
624 i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
625 if i > 0:
626 return True
627 else:
628 return False
630 def set_ip(self, child):
631 """fix the IP address to the same value it had when we
632 connected, but don't use DHCP, and force the DNS server to our
633 DNS server. This allows DNS updates to run"""
634 self.get_ipconfig(child)
635 if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
636 raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
637 self.getvar("WIN_IP")))
638 child.sendline('netsh')
639 child.expect('netsh>')
640 child.sendline('offline')
641 child.expect('netsh>')
642 child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
643 child.expect('netsh>')
644 child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
645 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)
646 if i == 0:
647 child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
648 child.expect('netsh>')
649 child.sendline('commit')
650 child.sendline('online')
651 child.sendline('exit')
653 child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
654 return True
656 def resolve_ip(self, hostname, retries=60, delay=5):
657 '''resolve an IP given a hostname, assuming NBT'''
658 while retries > 0:
659 child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
660 i = 0
661 while i == 0:
662 i = child.expect(["querying", r'\d+.\d+.\d+.\d+', hostname, "Lookup failed"])
663 if i == 0:
664 child.expect("\r")
665 if i == 1:
666 return child.after
667 retries -= 1
668 time.sleep(delay)
669 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
670 raise RuntimeError("Failed to resolve IP of %s" % hostname)
672 def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
673 disable_firewall=True, run_tlntadmn=True, set_noexpire=False):
674 '''open a telnet connection to a windows server, return the pexpect child'''
675 set_route = False
676 set_dns = False
677 set_telnetclients = True
678 start_telnet = True
679 if self.getvar('WIN_IP'):
680 ip = self.getvar('WIN_IP')
681 else:
682 ip = self.resolve_ip(hostname)
683 self.setvar('WIN_IP', ip)
684 while retries > 0:
685 child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
686 i = child.expect(["Welcome to Microsoft Telnet Service",
687 "Denying new connections due to the limit on number of connections",
688 "No more connections are allowed to telnet server",
689 "Unable to connect to remote host",
690 "No route to host",
691 "Connection refused",
692 pexpect.EOF])
693 if i != 0:
694 child.close()
695 time.sleep(delay)
696 retries -= 1
697 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
698 continue
699 child.expect("password:")
700 child.sendline(password)
701 i = child.expect(["C:",
702 "TelnetClients",
703 "Denying new connections due to the limit on number of connections",
704 "No more connections are allowed to telnet server",
705 "Unable to connect to remote host",
706 "No route to host",
707 "Connection refused",
708 pexpect.EOF])
709 if i == 1:
710 if set_telnetclients:
711 self.run_cmd('bin/net rpc group add TelnetClients -S $WIN_IP -U$WIN_USER%$WIN_PASS')
712 self.run_cmd('bin/net rpc group addmem TelnetClients "authenticated users" -S $WIN_IP -U$WIN_USER%$WIN_PASS')
713 child.close()
714 retries -= 1
715 set_telnetclients = False
716 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
717 continue
718 else:
719 raise RuntimeError("Failed to connect with telnet due to missing TelnetClients membership")
721 if i == 6:
722 # This only works if it is installed and enabled, but not started. Not entirely likely, but possible
723 self.run_cmd('bin/net rpc service start TlntSvr -S $WIN_IP -U$WIN_USER%$WIN_PASS')
724 child.close()
725 start_telnet = False
726 retries -= 1
727 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
728 continue
730 if i != 0:
731 child.close()
732 time.sleep(delay)
733 retries -= 1
734 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
735 continue
736 if set_dns:
737 set_dns = False
738 if self.set_dns(child):
739 continue
740 if set_route:
741 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
742 child.expect("C:")
743 set_route = False
744 if set_time:
745 self.run_date_time(child, None)
746 set_time = False
747 if run_tlntadmn:
748 self.run_tlntadmn(child)
749 run_tlntadmn = False
750 if set_noexpire:
751 self.set_noexpire(child, username)
752 set_noexpire = False
753 if disable_firewall:
754 self.disable_firewall(child)
755 disable_firewall = False
756 if set_ip:
757 set_ip = False
758 if self.set_ip(child):
759 set_route = True
760 set_dns = True
761 continue
762 return child
763 raise RuntimeError("Failed to connect with telnet")
765 def kinit(self, username, password):
766 '''use kinit to setup a credentials cache'''
767 self.run_cmd("kdestroy")
768 self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
769 username = self.substitute(username)
770 s = username.split('@')
771 if len(s) > 0:
772 s[1] = s[1].upper()
773 username = '@'.join(s)
774 child = self.pexpect_spawn('kinit ' + username)
775 child.expect("Password")
776 child.sendline(password)
777 child.expect(pexpect.EOF)
778 child.close()
779 if child.exitstatus != 0:
780 raise RuntimeError("kinit failed with status %d" % child.exitstatus)
782 def get_domains(self):
783 '''return a dictionary of DNS domains and IPs for named.conf'''
784 ret = {}
785 for v in self.vars:
786 if v[-6:] == "_REALM":
787 base = v[:-6]
788 if base + '_IP' in self.vars:
789 ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
790 return ret
792 def wait_reboot(self, retries=3):
793 '''wait for a VM to reboot'''
795 # first wait for it to shutdown
796 self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
798 # now wait for it to come back. If it fails to come back
799 # then try resetting it
800 while retries > 0:
801 try:
802 self.port_wait("${WIN_IP}", 139)
803 return
804 except:
805 retries -= 1
806 self.vm_reset("${WIN_VM}")
807 self.info("retrying reboot (retries=%u)" % retries)
808 raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
810 def get_vms(self):
811 '''return a dictionary of all the configured VM names'''
812 ret = []
813 for v in self.vars:
814 if v[-3:] == "_VM":
815 ret.append(self.vars[v])
816 return ret
818 def run_dcpromo_as_first_dc(self, vm, func_level=None):
819 self.setwinvars(vm)
820 self.info("Configuring a windows VM ${WIN_VM} at the first DC in the domain using dcpromo")
821 child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_time=True)
822 if self.get_is_dc(child):
823 return
825 if func_level == '2008r2':
826 self.setvar("FUNCTION_LEVEL_INT", str(4))
827 elif func_level == '2003':
828 self.setvar("FUNCTION_LEVEL_INT", str(1))
829 else:
830 self.setvar("FUNCTION_LEVEL_INT", str(0))
832 child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_ip=True, set_noexpire=True)
834 """This server must therefore not yet be a directory server, so we must promote it"""
835 child.sendline("copy /Y con answers.txt")
836 child.sendline(br'''
837 [DCInstall]
838 ; New forest promotion
839 ReplicaOrNewDomain=Domain
840 NewDomain=Forest
841 NewDomainDNSName=${WIN_REALM}
842 ForestLevel=${FUNCTION_LEVEL_INT}
843 DomainNetbiosName=${WIN_DOMAIN}
844 DomainLevel=${FUNCTION_LEVEL_INT}
845 InstallDNS=Yes
846 ConfirmGc=Yes
847 CreateDNSDelegation=No
848 DatabasePath="C:\Windows\NTDS"
849 LogPath="C:\Windows\NTDS"
850 SYSVOLPath="C:\Windows\SYSVOL"
851 ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
852 SafeModeAdminPassword=${WIN_PASS}
853 ; Run-time flags (optional)
854 RebootOnCompletion=No
855 \x1a
856 ''')
857 child.expect("copied.")
858 child.expect("C:")
859 child.expect("C:")
860 child.sendline("dcpromo /answer:answers.txt")
861 i = child.expect(["You must restart this computer", "failed", "Active Directory Domain Services was not installed", "C:", pexpect.TIMEOUT], timeout=240)
862 if i == 1 or i == 2:
863 raise Exception("dcpromo failed")
864 if i == 4: # timeout
865 child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}")
867 child.sendline("shutdown -r -t 0")
868 self.port_wait("${WIN_IP}", 139, wait_for_fail=True)
869 self.port_wait("${WIN_IP}", 139)
871 child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}")
872 # Check if we became a DC by now
873 if not self.get_is_dc(child):
874 raise Exception("dcpromo failed (and wasn't a DC even after rebooting)")
875 # Give DNS registration a kick
876 child.sendline("ipconfig /registerdns")
878 self.retry_cmd("host -t SRV _ldap._tcp.${WIN_REALM} ${WIN_IP}", ['has SRV record'], retries=60, delay=5)
880 def start_winvm(self, vm):
881 '''start a Windows VM'''
882 self.setwinvars(vm)
884 self.info("Joining a windows box to the domain")
885 self.vm_poweroff("${WIN_VM}", checkfail=False)
886 self.vm_restore("${WIN_VM}", "${WIN_SNAPSHOT}")
888 def run_winjoin(self, vm, domain, username="administrator", password="${PASSWORD1}"):
889 '''join a windows box to a domain'''
890 child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True, set_noexpire=True)
891 retries = 5
892 while retries > 0:
893 child.sendline("ipconfig /flushdns")
894 child.expect("C:")
895 child.sendline("netdom join ${WIN_HOSTNAME} /Domain:%s /UserD:%s /PasswordD:%s" % (domain, username, password))
896 i = child.expect(["The command completed successfully",
897 "The specified domain either does not exist or could not be contacted."], timeout=120)
898 if i == 0:
899 break
900 time.sleep(10)
901 retries -= 1
903 child.expect("C:")
904 child.sendline("shutdown /r -t 0")
905 self.wait_reboot()
906 child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True)
907 child.sendline("ipconfig /registerdns")
908 child.expect("Registration of the DNS resource records for all adapters of this computer has been initiated. Any errors will be reported in the Event Viewer")
909 child.expect("C:")
911 def test_remote_smbclient(self, vm, username="${WIN_USER}", password="${WIN_PASS}", args=""):
912 '''test smbclient against remote server'''
913 self.setwinvars(vm)
914 self.info('Testing smbclient')
915 self.chdir('${PREFIX}')
916 smbclient = self.getvar("smbclient")
917 self.cmd_contains("%s --version" % (smbclient), ["${SAMBA_VERSION}"])
918 self.retry_cmd('%s -L ${WIN_HOSTNAME} -U%s%%%s %s' % (smbclient, username, password, args), ["IPC"], retries=60, delay=5)
920 def test_net_use(self, vm, realm, domain, username, password):
921 self.setwinvars(vm)
922 self.info('Testing net use against Samba3 member')
923 child = self.open_telnet("${WIN_HOSTNAME}", "%s\\%s" % (domain, username), password)
924 child.sendline("net use t: \\\\${HOSTNAME}.%s\\test" % realm)
925 child.expect("The command completed successfully")
927 def setup(self, testname, subdir):
928 '''setup for main tests, parsing command line'''
929 self.parser.add_option("--conf", type='string', default='', help='config file')
930 self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
931 self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
932 self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
933 self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
934 self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
935 self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
936 self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
937 self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
938 self.parser.add_option("--use-ntvfs", action='store_true', default=False, help='use NTVFS for the fileserver')
939 self.parser.add_option("--dns-backend", type="choice",
940 choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
941 help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
942 "BIND9_FLATFILE uses bind9 text database to store zone information, "
943 "BIND9_DLZ uses samba4 AD to store zone information, "
944 "NONE skips the DNS setup entirely (not recommended)",
945 default="SAMBA_INTERNAL")
947 self.opts, self.args = self.parser.parse_args()
949 if not self.opts.conf:
950 print("Please specify a config file with --conf")
951 sys.exit(1)
953 # we don't need fsync safety in these tests
954 self.putenv('TDB_NO_FSYNC', '1')
956 self.load_config(self.opts.conf)
958 nameserver = self.get_nameserver()
959 if nameserver == self.getvar('NAMED_INTERFACE_IP'):
960 raise RuntimeError("old /etc/resolv.conf must not contain %s as a nameserver, this will create loops with the generated dns configuration" % nameserver)
961 self.setvar('DNSSERVER', nameserver)
963 self.set_skip(self.opts.skip)
964 self.set_vms(self.opts.vms)
966 if self.opts.list:
967 self.list_steps_mode()
969 if self.opts.prefix:
970 self.setvar('PREFIX', self.opts.prefix)
972 if self.opts.sourcetree:
973 self.setvar('SOURCETREE', self.opts.sourcetree)
975 if self.opts.rebase:
976 self.info('rebasing')
977 self.chdir('${SOURCETREE}')
978 self.run_cmd('git pull --rebase')
980 if self.opts.clean:
981 self.info('cleaning')
982 self.chdir('${SOURCETREE}/' + subdir)
983 self.run_cmd('make clean')
985 if self.opts.use_ntvfs:
986 self.setvar('USE_NTVFS', "--use-ntvfs")
987 else:
988 self.setvar('USE_NTVFS', "")
990 self.setvar('NAMESERVER_BACKEND', self.opts.dns_backend)
992 self.setvar('DNS_FORWARDER', "--option=dns forwarder=%s" % nameserver)