smbldap: expose bind callback via API and increase smbldap ABI version
[vlendec/samba-autobuild/.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         start_telnet = True
683         if self.getvar('WIN_IP'):
684             ip = self.getvar('WIN_IP')
685         else:
686             ip = self.resolve_ip(hostname)
687             self.setvar('WIN_IP', ip)
688         while retries > 0:
689             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
690             i = child.expect(["Welcome to Microsoft Telnet Service",
691                               "Denying new connections due to the limit on number of connections",
692                               "No more connections are allowed to telnet server",
693                               "Unable to connect to remote host",
694                               "No route to host",
695                               "Connection refused",
696                               pexpect.EOF])
697             if i != 0:
698                 child.close()
699                 time.sleep(delay)
700                 retries -= 1
701                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
702                 continue
703             child.expect("password:")
704             child.sendline(password)
705             i = child.expect(["C:",
706                               "TelnetClients",
707                               "Denying new connections due to the limit on number of connections",
708                               "No more connections are allowed to telnet server",
709                               "Unable to connect to remote host",
710                               "No route to host",
711                               "Connection refused",
712                               pexpect.EOF])
713             if i == 1:
714                 if set_telnetclients:
715                     self.run_cmd('bin/net rpc group add TelnetClients -S $WIN_IP -U$WIN_USER%$WIN_PASS')
716                     self.run_cmd('bin/net rpc group addmem TelnetClients "authenticated users" -S $WIN_IP -U$WIN_USER%$WIN_PASS')
717                     child.close()
718                     retries -= 1
719                     set_telnetclients = False
720                     self.info("retrying (retries=%u delay=%u)" % (retries, delay))
721                     continue
722                 else:
723                     raise RuntimeError("Failed to connect with telnet due to missing TelnetClients membership")
724
725             if i == 6:
726                 # This only works if it is installed and enabled, but not started.  Not entirely likely, but possible
727                 self.run_cmd('bin/net rpc service start TlntSvr -S $WIN_IP -U$WIN_USER%$WIN_PASS')
728                 child.close()
729                 start_telnet = False
730                 retries -= 1
731                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
732                 continue
733
734             if i != 0:
735                 child.close()
736                 time.sleep(delay)
737                 retries -= 1
738                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
739                 continue
740             if set_dns:
741                 set_dns = False
742                 if self.set_dns(child):
743                     continue;
744             if set_route:
745                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
746                 child.expect("C:")
747                 set_route = False
748             if set_time:
749                 self.run_date_time(child, None)
750                 set_time = False
751             if run_tlntadmn:
752                 self.run_tlntadmn(child)
753                 run_tlntadmn = False
754             if set_noexpire:
755                 self.set_noexpire(child, username)
756                 set_noexpire = False
757             if disable_firewall:
758                 self.disable_firewall(child)
759                 disable_firewall = False
760             if set_ip:
761                 set_ip = False
762                 if self.set_ip(child):
763                     set_route = True
764                     set_dns = True
765                 continue
766             return child
767         raise RuntimeError("Failed to connect with telnet")
768
769     def kinit(self, username, password):
770         '''use kinit to setup a credentials cache'''
771         self.run_cmd("kdestroy")
772         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
773         username = self.substitute(username)
774         s = username.split('@')
775         if len(s) > 0:
776             s[1] = s[1].upper()
777         username = '@'.join(s)
778         child = self.pexpect_spawn('kinit ' + username)
779         child.expect("Password")
780         child.sendline(password)
781         child.expect(pexpect.EOF)
782         child.close()
783         if child.exitstatus != 0:
784             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
785
786     def get_domains(self):
787         '''return a dictionary of DNS domains and IPs for named.conf'''
788         ret = {}
789         for v in self.vars:
790             if v[-6:] == "_REALM":
791                 base = v[:-6]
792                 if base + '_IP' in self.vars:
793                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
794         return ret
795
796     def wait_reboot(self, retries=3):
797         '''wait for a VM to reboot'''
798
799         # first wait for it to shutdown
800         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
801
802         # now wait for it to come back. If it fails to come back
803         # then try resetting it
804         while retries > 0:
805             try:
806                 self.port_wait("${WIN_IP}", 139)
807                 return
808             except:
809                 retries -= 1
810                 self.vm_reset("${WIN_VM}")
811                 self.info("retrying reboot (retries=%u)" % retries)
812         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
813
814     def get_vms(self):
815         '''return a dictionary of all the configured VM names'''
816         ret = []
817         for v in self.vars:
818             if v[-3:] == "_VM":
819                 ret.append(self.vars[v])
820         return ret
821
822
823     def run_dcpromo_as_first_dc(self, vm, func_level=None):
824         self.setwinvars(vm)
825         self.info("Configuring a windows VM ${WIN_VM} at the first DC in the domain using dcpromo")
826         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_time=True)
827         if self.get_is_dc(child):
828             return
829
830         if func_level == '2008r2':
831             self.setvar("FUNCTION_LEVEL_INT", str(4))
832         elif func_level == '2003':
833             self.setvar("FUNCTION_LEVEL_INT", str(1))
834         else:
835             self.setvar("FUNCTION_LEVEL_INT", str(0))
836
837         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_ip=True, set_noexpire=True)
838
839         """This server must therefore not yet be a directory server, so we must promote it"""
840         child.sendline("copy /Y con answers.txt")
841         child.sendline('''
842 [DCInstall]
843 ; New forest promotion
844 ReplicaOrNewDomain=Domain
845 NewDomain=Forest
846 NewDomainDNSName=${WIN_REALM}
847 ForestLevel=${FUNCTION_LEVEL_INT}
848 DomainNetbiosName=${WIN_DOMAIN}
849 DomainLevel=${FUNCTION_LEVEL_INT}
850 InstallDNS=Yes
851 ConfirmGc=Yes
852 CreateDNSDelegation=No
853 DatabasePath="C:\Windows\NTDS"
854 LogPath="C:\Windows\NTDS"
855 SYSVOLPath="C:\Windows\SYSVOL"
856 ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
857 SafeModeAdminPassword=${WIN_PASS}
858 ; Run-time flags (optional)
859 RebootOnCompletion=No
860 \1a
861 ''')
862         child.expect("copied.")
863         child.expect("C:")
864         child.expect("C:")
865         child.sendline("dcpromo /answer:answers.txt")
866         i = child.expect(["You must restart this computer", "failed", "Active Directory Domain Services was not installed", "C:", pexpect.TIMEOUT], timeout=240)
867         if i == 1 or i == 2:
868             raise Exception("dcpromo failed")
869         if i == 4: # timeout
870             child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}")
871
872         child.sendline("shutdown -r -t 0")
873         self.port_wait("${WIN_IP}", 139, wait_for_fail=True)
874         self.port_wait("${WIN_IP}", 139)
875
876         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}")
877         # Check if we became a DC by now
878         if not self.get_is_dc(child):
879             raise Exception("dcpromo failed (and wasn't a DC even after rebooting)")
880         # Give DNS registration a kick
881         child.sendline("ipconfig /registerdns")
882
883         self.retry_cmd("host -t SRV _ldap._tcp.${WIN_REALM} ${WIN_IP}", ['has SRV record'], retries=60, delay=5 )
884
885
886     def start_winvm(self, vm):
887         '''start a Windows VM'''
888         self.setwinvars(vm)
889         
890         self.info("Joining a windows box to the domain")
891         self.vm_poweroff("${WIN_VM}", checkfail=False)
892         self.vm_restore("${WIN_VM}", "${WIN_SNAPSHOT}")
893
894     def run_winjoin(self, vm, domain, username="administrator", password="${PASSWORD1}"):
895         '''join a windows box to a domain'''
896         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True, set_noexpire=True)
897         retries = 5
898         while retries > 0:
899             child.sendline("ipconfig /flushdns")
900             child.expect("C:")
901             child.sendline("netdom join ${WIN_HOSTNAME} /Domain:%s /UserD:%s /PasswordD:%s" % (domain, username, password))
902             i = child.expect(["The command completed successfully", 
903                              "The specified domain either does not exist or could not be contacted."], timeout=120)
904             if i == 0:
905                 break
906             time.sleep(10)
907             retries -= 1
908
909         child.expect("C:")
910         child.sendline("shutdown /r -t 0")
911         self.wait_reboot()
912         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True)
913         child.sendline("ipconfig /registerdns")
914         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")
915         child.expect("C:")
916
917
918     def test_remote_smbclient(self, vm, username="${WIN_USER}", password="${WIN_PASS}", args=""):
919         '''test smbclient against remote server'''
920         self.setwinvars(vm)
921         self.info('Testing smbclient')
922         self.chdir('${PREFIX}')
923         smbclient = self.getvar("smbclient")
924         self.cmd_contains("%s --version" % (smbclient), ["${SAMBA_VERSION}"])
925         self.retry_cmd('%s -L ${WIN_HOSTNAME} -U%s%%%s %s' % (smbclient, username, password, args), ["IPC"], retries=60, delay=5)
926
927     def test_net_use(self, vm, realm, domain, username, password):
928         self.setwinvars(vm)
929         self.info('Testing net use against Samba3 member')
930         child = self.open_telnet("${WIN_HOSTNAME}", "%s\\%s" % (domain, username), password)
931         child.sendline("net use t: \\\\${HOSTNAME}.%s\\test" % realm)
932         child.expect("The command completed successfully")
933
934
935     def setup(self, testname, subdir):
936         '''setup for main tests, parsing command line'''
937         self.parser.add_option("--conf", type='string', default='', help='config file')
938         self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
939         self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
940         self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
941         self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
942         self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
943         self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
944         self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
945         self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
946         self.parser.add_option("--use-ntvfs", action='store_true', default=False, help='use NTVFS for the fileserver')
947         self.parser.add_option("--dns-backend", type="choice",
948             choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
949             help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " \
950                  "BIND9_FLATFILE uses bind9 text database to store zone information, " \
951                  "BIND9_DLZ uses samba4 AD to store zone information, " \
952                  "NONE skips the DNS setup entirely (not recommended)",
953             default="SAMBA_INTERNAL")
954
955         self.opts, self.args = self.parser.parse_args()
956
957         if not self.opts.conf:
958             print("Please specify a config file with --conf")
959             sys.exit(1)
960
961         # we don't need fsync safety in these tests
962         self.putenv('TDB_NO_FSYNC', '1')
963
964         self.load_config(self.opts.conf)
965
966         nameserver = self.get_nameserver()
967         if nameserver == self.getvar('NAMED_INTERFACE_IP'):
968             raise RuntimeError("old /etc/resolv.conf must not contain %s as a nameserver, this will create loops with the generated dns configuration" % nameserver)
969         self.setvar('DNSSERVER', nameserver)
970
971         self.set_skip(self.opts.skip)
972         self.set_vms(self.opts.vms)
973
974         if self.opts.list:
975             self.list_steps_mode()
976
977         if self.opts.prefix:
978             self.setvar('PREFIX', self.opts.prefix)
979
980         if self.opts.sourcetree:
981             self.setvar('SOURCETREE', self.opts.sourcetree)
982
983         if self.opts.rebase:
984             self.info('rebasing')
985             self.chdir('${SOURCETREE}')
986             self.run_cmd('git pull --rebase')
987
988         if self.opts.clean:
989             self.info('cleaning')
990             self.chdir('${SOURCETREE}/' + subdir)
991             self.run_cmd('make clean')
992
993         if self.opts.use_ntvfs:
994             self.setvar('USE_NTVFS', "--use-ntvfs")
995         else:
996             self.setvar('USE_NTVFS', "")
997
998         self.setvar('NAMESERVER_BACKEND', self.opts.dns_backend)
999
1000         self.setvar('DNS_FORWARDER', "--option=dns forwarder=%s" % nameserver)