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