e9898620a1f207648b7f673e261a6f6a67bbf18b
[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         if self.getvar('INTERFACE_IPV6'):
329             ipv6_listen = 'listen-on-v6 port 53 { ${INTERFACE_IPV6}; };'
330         else:
331             ipv6_listen = ''
332         self.setvar('BIND_LISTEN_IPV6', ipv6_listen)
333
334         if not kerberos_support:
335             self.setvar("NAMED_TKEY_OPTION", "")
336         else:
337             if self.named_supports_gssapi_keytab():
338                 self.setvar("NAMED_TKEY_OPTION",
339                          'tkey-gssapi-keytab "${PREFIX}/private/dns.keytab";')
340             else:
341                 self.info("LCREALM=${LCREALM}")
342                 self.setvar("NAMED_TKEY_OPTION",
343                          '''tkey-gssapi-credential "DNS/${LCREALM}";
344                             tkey-domain "${LCREALM}";
345                  ''')
346             self.putenv('KEYTAB_FILE', '${PREFIX}/private/dns.keytab')
347             self.putenv('KRB5_KTNAME', '${PREFIX}/private/dns.keytab')
348
349         if include:
350             self.setvar("NAMED_INCLUDE", 'include "%s";' % include)
351         else:
352             self.setvar("NAMED_INCLUDE", '')
353
354         self.run_cmd("mkdir -p ${PREFIX}/etc")
355
356         self.write_file("etc/named.conf", '''
357 options {
358         listen-on port 53 { ${INTERFACE_IP};  };
359         ${BIND_LISTEN_IPV6}
360         directory       "${PREFIX}/var/named";
361         dump-file       "${PREFIX}/var/named/data/cache_dump.db";
362         pid-file        "${PREFIX}/var/named/named.pid";
363         statistics-file "${PREFIX}/var/named/data/named_stats.txt";
364         memstatistics-file "${PREFIX}/var/named/data/named_mem_stats.txt";
365         allow-query     { any; };
366         recursion yes;
367         ${NAMED_TKEY_OPTION}
368         max-cache-ttl 10;
369         max-ncache-ttl 10;
370
371         forward only;
372         forwarders {
373                   ${DNSSERVER};
374         };
375
376 };
377
378 key "rndc-key" {
379         algorithm hmac-md5;
380         secret "lA/cTrno03mt5Ju17ybEYw==";
381 };
382
383 controls {
384         inet ${INTERFACE_IP} port 953
385         allow { any; } keys { "rndc-key"; };
386 };
387
388 ${NAMED_INCLUDE}
389 ''')
390
391         # add forwarding for the windows domains
392         domains = self.get_domains()
393         for d in domains:
394             self.write_file('etc/named.conf',
395                          '''
396 zone "%s" IN {
397       type forward;
398       forward only;
399       forwarders {
400          %s;
401       };
402 };
403 ''' % (d, domains[d]),
404                      mode='a')
405
406
407         self.write_file("etc/rndc.conf", '''
408 # Start of rndc.conf
409 key "rndc-key" {
410         algorithm hmac-md5;
411         secret "lA/cTrno03mt5Ju17ybEYw==";
412 };
413
414 options {
415         default-key "rndc-key";
416         default-server  ${INTERFACE_IP};
417         default-port 953;
418 };
419 ''')
420
421
422     def stop_bind(self):
423         '''Stop our private BIND from listening and operating'''
424         self.rndc_cmd("stop", checkfail=False)
425         self.port_wait("${INTERFACE_IP}", 53, wait_for_fail=True)
426
427         self.run_cmd("rm -rf var/named")
428
429
430     def start_bind(self):
431         '''restart the test environment version of bind'''
432         self.info("Restarting bind9")
433         self.chdir('${PREFIX}')
434
435         self.run_cmd("mkdir -p var/named/data")
436         self.run_cmd("chown -R ${BIND_USER} var/named")
437
438         self.bind_child = self.run_child("${BIND9} -u ${BIND_USER} -n 1 -c ${PREFIX}/etc/named.conf -g")
439
440         self.port_wait("${INTERFACE_IP}", 53)
441         self.rndc_cmd("flush")
442
443     def restart_bind(self, kerberos_support=False, include=None):
444         self.configure_bind(kerberos_support=kerberos_support, include=include)
445         self.stop_bind()
446         self.start_bind()
447
448     def restore_resolv_conf(self):
449         '''restore the /etc/resolv.conf after testing is complete'''
450         if getattr(self, 'resolv_conf_backup', False):
451             self.info("restoring /etc/resolv.conf")
452             self.run_cmd("mv -f %s /etc/resolv.conf" % self.resolv_conf_backup)
453
454
455     def vm_poweroff(self, vmname, checkfail=True):
456         '''power off a VM'''
457         self.setvar('VMNAME', vmname)
458         self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
459
460     def vm_reset(self, vmname):
461         '''reset a VM'''
462         self.setvar('VMNAME', vmname)
463         self.run_cmd("${VM_RESET}")
464
465     def vm_restore(self, vmname, snapshot):
466         '''restore a VM'''
467         self.setvar('VMNAME', vmname)
468         self.setvar('SNAPSHOT', snapshot)
469         self.run_cmd("${VM_RESTORE}")
470
471     def ping_wait(self, hostname):
472         '''wait for a hostname to come up on the network'''
473         hostname = self.substitute(hostname)
474         loops=10
475         while loops > 0:
476             try:
477                 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
478                 break
479             except:
480                 loops = loops - 1
481         if loops == 0:
482             raise RuntimeError("Failed to ping %s" % hostname)
483         self.info("Host %s is up" % hostname)
484
485     def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
486         '''wait for a host to come up on the network'''
487
488         while retries > 0:
489             child = self.pexpect_spawn("nc -v -z -w 1 %s %u" % (hostname, port), crlf=False, timeout=1)
490             child.expect([pexpect.EOF, pexpect.TIMEOUT])
491             child.close()
492             i = child.exitstatus
493             if wait_for_fail:
494                 #wait for timeout or fail
495                 if i == None or i > 0:
496                     return
497             else:
498                 if i == 0:
499                     return
500
501             time.sleep(delay)
502             retries -= 1
503             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
504
505         raise RuntimeError("gave up waiting for %s:%d" % (hostname, port))
506
507     def run_net_time(self, child):
508         '''run net time on windows'''
509         child.sendline("net time \\\\${HOSTNAME} /set")
510         child.expect("Do you want to set the local computer")
511         child.sendline("Y")
512         child.expect("The command completed successfully")
513
514     def run_date_time(self, child, time_tuple=None):
515         '''run date and time on windows'''
516         if time_tuple is None:
517             time_tuple = time.localtime()
518         child.sendline("date")
519         child.expect("Enter the new date:")
520         i = child.expect(["dd-mm-yy", "mm-dd-yy"])
521         if i == 0:
522             child.sendline(time.strftime("%d-%m-%y", time_tuple))
523         else:
524             child.sendline(time.strftime("%m-%d-%y", time_tuple))
525         child.expect("C:")
526         child.sendline("time")
527         child.expect("Enter the new time:")
528         child.sendline(time.strftime("%H:%M:%S", time_tuple))
529         child.expect("C:")
530
531     def get_ipconfig(self, child):
532         '''get the IP configuration of the child'''
533         child.sendline("ipconfig /all")
534         child.expect('Ethernet adapter ')
535         child.expect("[\w\s]+")
536         self.setvar("WIN_NIC", child.after)
537         child.expect(['IPv4 Address', 'IP Address'])
538         child.expect('\d+.\d+.\d+.\d+')
539         self.setvar('WIN_IPV4_ADDRESS', child.after)
540         child.expect('Subnet Mask')
541         child.expect('\d+.\d+.\d+.\d+')
542         self.setvar('WIN_SUBNET_MASK', child.after)
543         child.expect('Default Gateway')
544         i = child.expect(['\d+.\d+.\d+.\d+', "C:"])
545         if i == 0:
546             self.setvar('WIN_DEFAULT_GATEWAY', child.after)
547             child.expect("C:")
548
549     def get_is_dc(self, child):
550         '''check if a windows machine is a domain controller'''
551         child.sendline("dcdiag")
552         i = child.expect(["is not a [Directory Server|DC]",
553                           "is not recognized as an internal or external command",
554                           "Home Server = ",
555                           "passed test Replications"])
556         if i == 0:
557             return False
558         if i == 1 or i == 3:
559             child.expect("C:")
560             child.sendline("net config Workstation")
561             child.expect("Workstation domain")
562             child.expect('[\S]+')
563             domain = child.after
564             i = child.expect(["Workstation Domain DNS Name", "Logon domain"])
565             '''If we get the Logon domain first, we are not in an AD domain'''
566             if i == 1:
567                 return False
568             if domain.upper() == self.getvar("WIN_DOMAIN").upper():
569                 return True
570
571         child.expect('[\S]+')
572         hostname = child.after
573         if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
574             return True
575
576     def set_noexpire(self, child, username):
577         """Ensure this user's password does not expire"""
578         child.sendline('wmic useraccount where name="%s" set PasswordExpires=FALSE' % username)
579         child.expect("update successful")
580         child.expect("C:")
581
582     def run_tlntadmn(self, child):
583         '''remove the annoying telnet restrictions'''
584         child.sendline('tlntadmn config maxconn=1024')
585         child.expect(["The settings were successfully updated", "Access is denied"])
586         child.expect("C:")
587
588     def disable_firewall(self, child):
589         '''remove the annoying firewall'''
590         child.sendline('netsh advfirewall set allprofiles state off')
591         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off", "The requested operation requires elevation", "Access is denied"])
592         child.expect("C:")
593         if i == 1:
594             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
595             i = child.expect(["Ok", "The following command was not found", "Access is denied"])
596             if i != 0:
597                 self.info("Firewall disable failed - ignoring")
598             child.expect("C:")
599
600     def set_dns(self, child):
601         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
602         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
603         if i > 0:
604             return True
605         else:
606             return False
607
608     def set_ip(self, child):
609         """fix the IP address to the same value it had when we
610         connected, but don't use DHCP, and force the DNS server to our
611         DNS server.  This allows DNS updates to run"""
612         self.get_ipconfig(child)
613         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
614             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
615                                                                                 self.getvar("WIN_IP")))
616         child.sendline('netsh')
617         child.expect('netsh>')
618         child.sendline('offline')
619         child.expect('netsh>')
620         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
621         child.expect('netsh>')
622         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
623         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)
624         if i == 0:
625             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
626             child.expect('netsh>')
627         child.sendline('commit')
628         child.sendline('online')
629         child.sendline('exit')
630
631         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
632         return True
633
634
635     def resolve_ip(self, hostname, retries=60, delay=5):
636         '''resolve an IP given a hostname, assuming NBT'''
637         while retries > 0:
638             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
639             i = 0
640             while i == 0:
641                 i = child.expect(["querying", '\d+.\d+.\d+.\d+', hostname, "Lookup failed"])
642                 if i == 0:
643                     child.expect("\r")
644             if i == 1:
645                 return child.after
646             retries -= 1
647             time.sleep(delay)
648             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
649         raise RuntimeError("Failed to resolve IP of %s" % hostname)
650
651
652     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
653                     disable_firewall=True, run_tlntadmn=True, set_noexpire=False):
654         '''open a telnet connection to a windows server, return the pexpect child'''
655         set_route = False
656         set_dns = False
657         set_telnetclients = True
658         if self.getvar('WIN_IP'):
659             ip = self.getvar('WIN_IP')
660         else:
661             ip = self.resolve_ip(hostname)
662             self.setvar('WIN_IP', ip)
663         while retries > 0:
664             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
665             i = child.expect(["Welcome to Microsoft Telnet Service",
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             child.expect("password:")
679             child.sendline(password)
680             i = child.expect(["C:",
681                               "TelnetClients",
682                               "Denying new connections due to the limit on number of connections",
683                               "No more connections are allowed to telnet server",
684                               "Unable to connect to remote host",
685                               "No route to host",
686                               "Connection refused",
687                               pexpect.EOF])
688             if i == 1:
689                 if set_telnetclients:
690                     self.run_cmd('bin/net rpc group addmem TelnetClients "authenticated users" -S $WIN_IP -U$WIN_USER%$WIN_PASS')
691                     child.close()
692                     retries -= 1
693                     set_telnetclients = False
694                     self.info("retrying (retries=%u delay=%u)" % (retries, delay))
695                     continue
696                 else:
697                     raise RuntimeError("Failed to connect with telnet due to missing TelnetClients membership")
698
699             if i != 0:
700                 child.close()
701                 time.sleep(delay)
702                 retries -= 1
703                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
704                 continue
705             if set_dns:
706                 set_dns = False
707                 if self.set_dns(child):
708                     continue;
709             if set_route:
710                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
711                 child.expect("C:")
712                 set_route = False
713             if set_time:
714                 self.run_date_time(child, None)
715                 set_time = False
716             if run_tlntadmn:
717                 self.run_tlntadmn(child)
718                 run_tlntadmn = False
719             if set_noexpire:
720                 self.set_noexpire(child, username)
721                 set_noexpire = False
722             if disable_firewall:
723                 self.disable_firewall(child)
724                 disable_firewall = False
725             if set_ip:
726                 set_ip = False
727                 if self.set_ip(child):
728                     set_route = True
729                     set_dns = True
730                 continue
731             return child
732         raise RuntimeError("Failed to connect with telnet")
733
734     def kinit(self, username, password):
735         '''use kinit to setup a credentials cache'''
736         self.run_cmd("kdestroy")
737         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
738         username = self.substitute(username)
739         s = username.split('@')
740         if len(s) > 0:
741             s[1] = s[1].upper()
742         username = '@'.join(s)
743         child = self.pexpect_spawn('kinit ' + username)
744         child.expect("Password")
745         child.sendline(password)
746         child.expect(pexpect.EOF)
747         child.close()
748         if child.exitstatus != 0:
749             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
750
751     def get_domains(self):
752         '''return a dictionary of DNS domains and IPs for named.conf'''
753         ret = {}
754         for v in self.vars:
755             if v[-6:] == "_REALM":
756                 base = v[:-6]
757                 if base + '_IP' in self.vars:
758                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
759         return ret
760
761     def wait_reboot(self, retries=3):
762         '''wait for a VM to reboot'''
763
764         # first wait for it to shutdown
765         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
766
767         # now wait for it to come back. If it fails to come back
768         # then try resetting it
769         while retries > 0:
770             try:
771                 self.port_wait("${WIN_IP}", 139)
772                 return
773             except:
774                 retries -= 1
775                 self.vm_reset("${WIN_VM}")
776                 self.info("retrying reboot (retries=%u)" % retries)
777         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
778
779     def get_vms(self):
780         '''return a dictionary of all the configured VM names'''
781         ret = []
782         for v in self.vars:
783             if v[-3:] == "_VM":
784                 ret.append(self.vars[v])
785         return ret
786
787
788     def run_dcpromo_as_first_dc(self, vm, func_level=None):
789         self.setwinvars(vm)
790         self.info("Configuring a windows VM ${WIN_VM} at the first DC in the domain using dcpromo")
791         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_time=True)
792         if self.get_is_dc(child):
793             return
794
795         if func_level == '2008r2':
796             self.setvar("FUNCTION_LEVEL_INT", str(4))
797         elif func_level == '2003':
798             self.setvar("FUNCTION_LEVEL_INT", str(1))
799         else:
800             self.setvar("FUNCTION_LEVEL_INT", str(0))
801
802         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_ip=True, set_noexpire=True)
803
804         """This server must therefore not yet be a directory server, so we must promote it"""
805         child.sendline("copy /Y con answers.txt")
806         child.sendline('''
807 [DCInstall]
808 ; New forest promotion
809 ReplicaOrNewDomain=Domain
810 NewDomain=Forest
811 NewDomainDNSName=${WIN_REALM}
812 ForestLevel=${FUNCTION_LEVEL_INT}
813 DomainNetbiosName=${WIN_DOMAIN}
814 DomainLevel=${FUNCTION_LEVEL_INT}
815 InstallDNS=Yes
816 ConfirmGc=Yes
817 CreateDNSDelegation=No
818 DatabasePath="C:\Windows\NTDS"
819 LogPath="C:\Windows\NTDS"
820 SYSVOLPath="C:\Windows\SYSVOL"
821 ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
822 SafeModeAdminPassword=${WIN_PASS}
823 ; Run-time flags (optional)
824 RebootOnCompletion=No
825 \1a
826 ''')
827         child.expect("copied.")
828         child.expect("C:")
829         child.expect("C:")
830         child.sendline("dcpromo /answer:answers.txt")
831         i = child.expect(["You must restart this computer", "failed", "Active Directory Domain Services was not installed", "C:"], timeout=240)
832         if i == 1 or i == 2:
833             raise Exception("dcpromo failed")
834         child.sendline("shutdown -r -t 0")
835         self.port_wait("${WIN_IP}", 139, wait_for_fail=True)
836         self.port_wait("${WIN_IP}", 139)
837         self.retry_cmd("host -t SRV _ldap._tcp.${WIN_REALM} ${WIN_IP}", ['has SRV record'], retries=60, delay=5 )
838
839
840     def start_winvm(self, vm):
841         '''start a Windows VM'''
842         self.setwinvars(vm)
843         
844         self.info("Joining a windows box to the domain")
845         self.vm_poweroff("${WIN_VM}", checkfail=False)
846         self.vm_restore("${WIN_VM}", "${WIN_SNAPSHOT}")
847
848     def run_winjoin(self, vm, domain, username="administrator", password="${PASSWORD1}"):
849         '''join a windows box to a domain'''
850         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True, set_noexpire=True)
851         retries = 5
852         while retries > 0:
853             child.sendline("ipconfig /flushdns")
854             child.expect("C:")
855             child.sendline("netdom join ${WIN_HOSTNAME} /Domain:%s /UserD:%s /PasswordD:%s" % (domain, username, password))
856             i = child.expect(["The command completed successfully", 
857                              "The specified domain either does not exist or could not be contacted."])
858             if i == 0:
859                 break
860             time.sleep(10)
861             retries -= 1
862
863         child.expect("C:")
864         child.sendline("shutdown /r -t 0")
865         self.wait_reboot()
866         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True)
867         child.sendline("ipconfig /registerdns")
868         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")
869         child.expect("C:")
870
871
872     def test_remote_smbclient(self, vm, username="${WIN_USER}", password="${WIN_PASS}", args=""):
873         '''test smbclient against remote server'''
874         self.setwinvars(vm)
875         self.info('Testing smbclient')
876         self.chdir('${PREFIX}')
877         smbclient = self.getvar("smbclient")
878         self.cmd_contains("%s --version" % (smbclient), ["${SAMBA_VERSION}"])
879         self.retry_cmd('%s -L ${WIN_HOSTNAME} -U%s%%%s %s' % (smbclient, username, password, args), ["IPC"], retries=60, delay=5)
880
881     def test_net_use(self, vm, realm, domain, username, password):
882         self.setwinvars(vm)
883         self.info('Testing net use against Samba3 member')
884         child = self.open_telnet("${WIN_HOSTNAME}", "%s\\%s" % (domain, username), password)
885         child.sendline("net use t: \\\\${HOSTNAME}.%s\\test" % realm)
886         child.expect("The command completed successfully")
887
888
889     def setup(self, testname, subdir):
890         '''setup for main tests, parsing command line'''
891         self.parser.add_option("--conf", type='string', default='', help='config file')
892         self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
893         self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
894         self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
895         self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
896         self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
897         self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
898         self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
899         self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
900         self.parser.add_option("--use-ntvfs", action='store_true', default=False, help='use NTVFS for the fileserver')
901         self.parser.add_option("--dns-backend", type="choice",
902             choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
903             help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " \
904                  "BIND9_FLATFILE uses bind9 text database to store zone information, " \
905                  "BIND9_DLZ uses samba4 AD to store zone information, " \
906                  "NONE skips the DNS setup entirely (not recommended)",
907             default="SAMBA_INTERNAL")
908
909         self.opts, self.args = self.parser.parse_args()
910
911         if not self.opts.conf:
912             print("Please specify a config file with --conf")
913             sys.exit(1)
914
915         # we don't need fsync safety in these tests
916         self.putenv('TDB_NO_FSYNC', '1')
917
918         self.load_config(self.opts.conf)
919
920         nameserver = self.get_nameserver()
921         if nameserver == self.getvar('INTERFACE_IP'):
922             raise RuntimeError("old /etc/resolv.conf must not contain %s as a nameserver, this will create loops with the generated dns configuration" % nameserver)
923         self.setvar('DNSSERVER', nameserver)
924
925         self.set_skip(self.opts.skip)
926         self.set_vms(self.opts.vms)
927
928         if self.opts.list:
929             self.list_steps_mode()
930
931         if self.opts.prefix:
932             self.setvar('PREFIX', self.opts.prefix)
933
934         if self.opts.sourcetree:
935             self.setvar('SOURCETREE', self.opts.sourcetree)
936
937         if self.opts.rebase:
938             self.info('rebasing')
939             self.chdir('${SOURCETREE}')
940             self.run_cmd('git pull --rebase')
941
942         if self.opts.clean:
943             self.info('cleaning')
944             self.chdir('${SOURCETREE}/' + subdir)
945             self.run_cmd('make clean')
946
947         if self.opts.use_ntvfs:
948             self.setvar('USE_NTVFS', "--use-ntvfs")
949         else:
950             self.setvar('USE_NTVFS', "")
951
952         self.setvar('NAMESERVER_BACKEND', self.opts.dns_backend)
953
954         if self.opts.dns_backend == 'SAMBA_INTERNAL':
955             self.setvar('ALLOW_DNS_UPDATES', '')
956             # we need recursive queries, since host expects answers with RA-bit
957             self.setvar('DNS_RECURSIVE_QUERIES', '--option="dns recursive queries=Yes"')
958             self.setvar('DNS_FORWARDER', '--option="dns forwarder=%s"' % nameserver)
959         else:
960             self.setvar('ALLOW_DNS_UPDATES', '')
961             self.setvar('DNS_RECURSIVE_QUERIES', '')
962             self.setvar('DNS_FORWARDER', '')