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