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