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