VERSION: Bump version up to Samba 4.17.13...
[samba.git] / wintest / wintest.py
1 #!/usr/bin/env python3
2
3 '''automated testing library for testing Samba against windows'''
4
5 import pexpect
6 import subprocess
7 import optparse
8 import sys
9 import os
10 import time
11 import re
12
13
14 class wintest():
15     '''testing of Samba against windows VMs'''
16
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")
23
24     def check_prerequesites(self):
25         self.info("Checking prerequesites")
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')
33
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')
38
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)
45
46     def setvar(self, varname, value):
47         '''set a substitution variable'''
48         self.vars[varname] = value
49
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]
55
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)
64
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")
72
73     def info(self, msg):
74         '''print some information'''
75         if not self.list_mode:
76             print(self.substitute(msg))
77
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)
91
92     def list_steps_mode(self):
93         '''put wintest in step listing mode'''
94         self.list_mode = True
95
96     def set_skip(self, skiplist):
97         '''set a list of tests to skip'''
98         self.skiplist = skiplist.split(',')
99
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)
107
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
114
115     def substitute(self, text):
116         """Substitute strings of the form ${NAME} in text, replacing
117         with substitutions from vars.
118         """
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
124
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])
139
140     def have_var(self, varname):
141         '''see if a variable has been set'''
142         return varname in self.vars
143
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
151
152     def putenv(self, key, value):
153         '''putenv with substitution'''
154         os.environ[key] = self.substitute(value)
155
156     def chdir(self, dir):
157         '''chdir with substitution'''
158         os.chdir(self.substitute(dir))
159
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)
164
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()
170
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)
188
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
205
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)
210
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'''
214
215         if isinstance(contains, str):
216             contains = [contains]
217
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:]
246
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)
260
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)
266
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)
272
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)
283
284         ret.old_sendline = ret.sendline
285         ret.sendline = sendline_sub
286         ret.old_expect = ret.expect
287         ret.expect = expect_sub
288
289         return ret
290
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('\d+.\d+.\d+.\d+')
299         return child.after
300
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)
304
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
314
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
320
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'
332
333     def configure_bind(self, kerberos_support=False, include=None):
334         self.chdir('${PREFIX}')
335
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)
341
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", "")
358
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", '')
363
364         self.run_cmd("mkdir -p ${PREFIX}/etc")
365
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;
380
381         forward only;
382         forwarders {
383                   ${DNSSERVER};
384         };
385
386 };
387
388 key "rndc-key" {
389         algorithm hmac-md5;
390         secret "lA/cTrno03mt5Ju17ybEYw==";
391 };
392
393 controls {
394         inet ${NAMED_INTERFACE_IP} port 953
395         allow { any; } keys { "rndc-key"; };
396 };
397
398 ${NAMED_INCLUDE}
399 ''')
400
401         if self.getvar('NAMESERVER_BACKEND') == 'SAMBA_INTERNAL':
402             self.write_file('etc/named.conf',
403                             '''
404 zone "%s" IN {
405       type forward;
406       forward only;
407       forwarders {
408          %s;
409       };
410 };
411 ''' % (self.getvar('LCREALM'), self.getvar('INTERFACE_IP')),
412                    mode='a')
413
414         # add forwarding for the windows domains
415         domains = self.get_domains()
416
417         for d in domains:
418             self.write_file('etc/named.conf',
419                             '''
420 zone "%s" IN {
421       type forward;
422       forward only;
423       forwarders {
424          %s;
425       };
426 };
427 ''' % (d, domains[d]),
428                      mode='a')
429
430         self.write_file("etc/rndc.conf", '''
431 # Start of rndc.conf
432 key "rndc-key" {
433         algorithm hmac-md5;
434         secret "lA/cTrno03mt5Ju17ybEYw==";
435 };
436
437 options {
438         default-key "rndc-key";
439         default-server  ${NAMED_INTERFACE_IP};
440         default-port 953;
441 };
442 ''')
443
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)
448
449         self.run_cmd("rm -rf var/named")
450
451     def start_bind(self):
452         '''restart the test environment version of bind'''
453         self.info("Restarting bind9")
454         self.chdir('${PREFIX}')
455
456         self.set_nameserver(self.getvar('NAMED_INTERFACE_IP'))
457
458         self.run_cmd("mkdir -p var/named/data")
459         self.run_cmd("chown -R ${BIND_USER} var/named")
460
461         self.bind_child = self.run_child("${BIND9} -u ${BIND_USER} -n 1 -c ${PREFIX}/etc/named.conf -g")
462
463         self.port_wait("${NAMED_INTERFACE_IP}", 53)
464         self.rndc_cmd("flush")
465
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()
470
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)
476
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)
481
482     def vm_reset(self, vmname):
483         '''reset a VM'''
484         self.setvar('VMNAME', vmname)
485         self.run_cmd("${VM_RESET}")
486
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}")
492
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)
506
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'''
509
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
522
523             time.sleep(delay)
524             retries -= 1
525             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
526
527         raise RuntimeError("gave up waiting for %s:%d" % (hostname, port))
528
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")
535
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:")
552
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("[\w\s]+")
558         self.setvar("WIN_NIC", child.after)
559         child.expect(['IPv4 Address', 'IP Address'])
560         child.expect('\d+.\d+.\d+.\d+')
561         self.setvar('WIN_IPV4_ADDRESS', child.after)
562         child.expect('Subnet Mask')
563         child.expect('\d+.\d+.\d+.\d+')
564         self.setvar('WIN_SUBNET_MASK', child.after)
565         child.expect('Default Gateway')
566         i = child.expect(['\d+.\d+.\d+.\d+', "C:"])
567         if i == 0:
568             self.setvar('WIN_DEFAULT_GATEWAY', child.after)
569             child.expect("C:")
570
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('[\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
592
593         child.expect('[\S]+')
594         hostname = child.after
595         if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
596             return True
597
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:")
603
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:")
609
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:")
621
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
629
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')
652
653         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
654         return True
655
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", '\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)
671
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")
720
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
729
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")
764
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)
781
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
791
792     def wait_reboot(self, retries=3):
793         '''wait for a VM to reboot'''
794
795         # first wait for it to shutdown
796         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
797
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"))
809
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
817
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
824
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))
831
832         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_ip=True, set_noexpire=True)
833
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(b'''
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 \1a
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}")
866
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)
870
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")
877
878         self.retry_cmd("host -t SRV _ldap._tcp.${WIN_REALM} ${WIN_IP}", ['has SRV record'], retries=60, delay=5)
879
880     def start_winvm(self, vm):
881         '''start a Windows VM'''
882         self.setwinvars(vm)
883
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}")
887
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
902
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:")
910
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)
919
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")
926
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")
946
947         self.opts, self.args = self.parser.parse_args()
948
949         if not self.opts.conf:
950             print("Please specify a config file with --conf")
951             sys.exit(1)
952
953         # we don't need fsync safety in these tests
954         self.putenv('TDB_NO_FSYNC', '1')
955
956         self.load_config(self.opts.conf)
957
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)
962
963         self.set_skip(self.opts.skip)
964         self.set_vms(self.opts.vms)
965
966         if self.opts.list:
967             self.list_steps_mode()
968
969         if self.opts.prefix:
970             self.setvar('PREFIX', self.opts.prefix)
971
972         if self.opts.sourcetree:
973             self.setvar('SOURCETREE', self.opts.sourcetree)
974
975         if self.opts.rebase:
976             self.info('rebasing')
977             self.chdir('${SOURCETREE}')
978             self.run_cmd('git pull --rebase')
979
980         if self.opts.clean:
981             self.info('cleaning')
982             self.chdir('${SOURCETREE}/' + subdir)
983             self.run_cmd('make clean')
984
985         if self.opts.use_ntvfs:
986             self.setvar('USE_NTVFS', "--use-ntvfs")
987         else:
988             self.setvar('USE_NTVFS', "")
989
990         self.setvar('NAMESERVER_BACKEND', self.opts.dns_backend)
991
992         self.setvar('DNS_FORWARDER', "--option=dns forwarder=%s" % nameserver)