wintest: add option to select the dns backend
[garming/samba-autobuild/.git] / wintest / wintest.py
1 #!/usr/bin/env python
2
3 '''automated testing library for testing Samba against windows'''
4
5 import pexpect, subprocess
6 import optparse
7 import sys, os, time, re
8
9 class wintest():
10     '''testing of Samba against windows VMs'''
11
12     def __init__(self):
13         self.vars = {}
14         self.list_mode = False
15         self.vms = None
16         os.environ['PYTHONUNBUFFERED'] = '1'
17         self.parser = optparse.OptionParser("wintest")
18
19     def check_prerequesites(self):
20         self.info("Checking prerequesites")
21         self.setvar('HOSTNAME', self.cmd_output("hostname -s").strip())
22         if os.getuid() != 0:
23             raise Exception("You must run this script as root")
24         self.run_cmd('ifconfig ${INTERFACE} ${INTERFACE_NET} up')
25         if self.getvar('INTERFACE_IPV6'):
26             self.run_cmd('ifconfig ${INTERFACE} inet6 del ${INTERFACE_IPV6}/64', checkfail=False)
27             self.run_cmd('ifconfig ${INTERFACE} inet6 add ${INTERFACE_IPV6}/64 up')
28
29     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             i = child.expect(['succeeded', 'failed', pexpect.EOF, pexpect.TIMEOUT])
496             if wait_for_fail:
497                 if i > 0:
498                     return
499             else:
500                 if i == 0:
501                     return
502
503             time.sleep(delay)
504             retries -= 1
505             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
506
507         raise RuntimeError("gave up waiting for %s:%d" % (hostname, port))
508
509     def run_net_time(self, child):
510         '''run net time on windows'''
511         child.sendline("net time \\\\${HOSTNAME} /set")
512         child.expect("Do you want to set the local computer")
513         child.sendline("Y")
514         child.expect("The command completed successfully")
515
516     def run_date_time(self, child, time_tuple=None):
517         '''run date and time on windows'''
518         if time_tuple is None:
519             time_tuple = time.localtime()
520         child.sendline("date")
521         child.expect("Enter the new date:")
522         i = child.expect(["dd-mm-yy", "mm-dd-yy"])
523         if i == 0:
524             child.sendline(time.strftime("%d-%m-%y", time_tuple))
525         else:
526             child.sendline(time.strftime("%m-%d-%y", time_tuple))
527         child.expect("C:")
528         child.sendline("time")
529         child.expect("Enter the new time:")
530         child.sendline(time.strftime("%H:%M:%S", time_tuple))
531         child.expect("C:")
532
533     def get_ipconfig(self, child):
534         '''get the IP configuration of the child'''
535         child.sendline("ipconfig /all")
536         child.expect('Ethernet adapter ')
537         child.expect("[\w\s]+")
538         self.setvar("WIN_NIC", child.after)
539         child.expect(['IPv4 Address', 'IP Address'])
540         child.expect('\d+.\d+.\d+.\d+')
541         self.setvar('WIN_IPV4_ADDRESS', child.after)
542         child.expect('Subnet Mask')
543         child.expect('\d+.\d+.\d+.\d+')
544         self.setvar('WIN_SUBNET_MASK', child.after)
545         child.expect('Default Gateway')
546         i = child.expect(['\d+.\d+.\d+.\d+', "C:"])
547         if i == 0:
548             self.setvar('WIN_DEFAULT_GATEWAY', child.after)
549             child.expect("C:")
550
551     def get_is_dc(self, child):
552         '''check if a windows machine is a domain controller'''
553         child.sendline("dcdiag")
554         i = child.expect(["is not a Directory Server",
555                           "is not recognized as an internal or external command",
556                           "Home Server = ",
557                           "passed test Replications"])
558         if i == 0:
559             return False
560         if i == 1 or i == 3:
561             child.expect("C:")
562             child.sendline("net config Workstation")
563             child.expect("Workstation domain")
564             child.expect('[\S]+')
565             domain = child.after
566             i = child.expect(["Workstation Domain DNS Name", "Logon domain"])
567             '''If we get the Logon domain first, we are not in an AD domain'''
568             if i == 1:
569                 return False
570             if domain.upper() == self.getvar("WIN_DOMAIN").upper():
571                 return True
572
573         child.expect('[\S]+')
574         hostname = child.after
575         if hostname.upper() == self.getvar("WIN_HOSTNAME").upper():
576             return True
577
578     def set_noexpire(self, child, username):
579         """Ensure this user's password does not expire"""
580         child.sendline('wmic useraccount where name="%s" set PasswordExpires=FALSE' % username)
581         child.expect("update successful")
582         child.expect("C:")
583
584     def run_tlntadmn(self, child):
585         '''remove the annoying telnet restrictions'''
586         child.sendline('tlntadmn config maxconn=1024')
587         child.expect(["The settings were successfully updated", "Access is denied"])
588         child.expect("C:")
589
590     def disable_firewall(self, child):
591         '''remove the annoying firewall'''
592         child.sendline('netsh advfirewall set allprofiles state off')
593         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off", "The requested operation requires elevation", "Access is denied"])
594         child.expect("C:")
595         if i == 1:
596             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
597             i = child.expect(["Ok", "The following command was not found", "Access is denied"])
598             if i != 0:
599                 self.info("Firewall disable failed - ignoring")
600             child.expect("C:")
601
602     def set_dns(self, child):
603         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
604         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
605         if i > 0:
606             return True
607         else:
608             return False
609
610     def set_ip(self, child):
611         """fix the IP address to the same value it had when we
612         connected, but don't use DHCP, and force the DNS server to our
613         DNS server.  This allows DNS updates to run"""
614         self.get_ipconfig(child)
615         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
616             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
617                                                                                 self.getvar("WIN_IP")))
618         child.sendline('netsh')
619         child.expect('netsh>')
620         child.sendline('offline')
621         child.expect('netsh>')
622         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
623         child.expect('netsh>')
624         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
625         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)
626         if i == 0:
627             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
628             child.expect('netsh>')
629         child.sendline('commit')
630         child.sendline('online')
631         child.sendline('exit')
632
633         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
634         return True
635
636
637     def resolve_ip(self, hostname, retries=60, delay=5):
638         '''resolve an IP given a hostname, assuming NBT'''
639         while retries > 0:
640             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
641             i = 0
642             while i == 0:
643                 i = child.expect(["querying", '\d+.\d+.\d+.\d+', hostname, "Lookup failed"])
644                 if i == 0:
645                     child.expect("\r")
646             if i == 1:
647                 return child.after
648             retries -= 1
649             time.sleep(delay)
650             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
651         raise RuntimeError("Failed to resolve IP of %s" % hostname)
652
653
654     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
655                     disable_firewall=True, run_tlntadmn=True, set_noexpire=False):
656         '''open a telnet connection to a windows server, return the pexpect child'''
657         set_route = False
658         set_dns = False
659         set_telnetclients = True
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                               "TelnetClients",
684                               "Denying new connections due to the limit on number of connections",
685                               "No more connections are allowed to telnet server",
686                               "Unable to connect to remote host",
687                               "No route to host",
688                               "Connection refused",
689                               pexpect.EOF])
690             if i == 1:
691                 if set_telnetclients:
692                     self.run_cmd('bin/net rpc group addmem TelnetClients "authenticated users" -S $WIN_IP -U$WIN_USER%$WIN_PASS')
693                     child.close()
694                     retries -= 1
695                     set_telnetclients = False
696                     self.info("retrying (retries=%u delay=%u)" % (retries, delay))
697                     continue
698                 else:
699                     raise RuntimeError("Failed to connect with telnet due to missing TelnetClients membership")
700
701             if i != 0:
702                 child.close()
703                 time.sleep(delay)
704                 retries -= 1
705                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
706                 continue
707             if set_dns:
708                 set_dns = False
709                 if self.set_dns(child):
710                     continue;
711             if set_route:
712                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
713                 child.expect("C:")
714                 set_route = False
715             if set_time:
716                 self.run_date_time(child, None)
717                 set_time = False
718             if run_tlntadmn:
719                 self.run_tlntadmn(child)
720                 run_tlntadmn = False
721             if set_noexpire:
722                 self.set_noexpire(child, username)
723                 set_noexpire = False
724             if disable_firewall:
725                 self.disable_firewall(child)
726                 disable_firewall = False
727             if set_ip:
728                 set_ip = False
729                 if self.set_ip(child):
730                     set_route = True
731                     set_dns = True
732                 continue
733             return child
734         raise RuntimeError("Failed to connect with telnet")
735
736     def kinit(self, username, password):
737         '''use kinit to setup a credentials cache'''
738         self.run_cmd("kdestroy")
739         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
740         username = self.substitute(username)
741         s = username.split('@')
742         if len(s) > 0:
743             s[1] = s[1].upper()
744         username = '@'.join(s)
745         child = self.pexpect_spawn('kinit ' + username)
746         child.expect("Password")
747         child.sendline(password)
748         child.expect(pexpect.EOF)
749         child.close()
750         if child.exitstatus != 0:
751             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
752
753     def get_domains(self):
754         '''return a dictionary of DNS domains and IPs for named.conf'''
755         ret = {}
756         for v in self.vars:
757             if v[-6:] == "_REALM":
758                 base = v[:-6]
759                 if base + '_IP' in self.vars:
760                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
761         return ret
762
763     def wait_reboot(self, retries=3):
764         '''wait for a VM to reboot'''
765
766         # first wait for it to shutdown
767         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
768
769         # now wait for it to come back. If it fails to come back
770         # then try resetting it
771         while retries > 0:
772             try:
773                 self.port_wait("${WIN_IP}", 139)
774                 return
775             except:
776                 retries -= 1
777                 self.vm_reset("${WIN_VM}")
778                 self.info("retrying reboot (retries=%u)" % retries)
779         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
780
781     def get_vms(self):
782         '''return a dictionary of all the configured VM names'''
783         ret = []
784         for v in self.vars:
785             if v[-3:] == "_VM":
786                 ret.append(self.vars[v])
787         return ret
788
789
790     def run_dcpromo_as_first_dc(self, vm, func_level=None):
791         self.setwinvars(vm)
792         self.info("Configuring a windows VM ${WIN_VM} at the first DC in the domain using dcpromo")
793         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_time=True)
794         if self.get_is_dc(child):
795             return
796
797         if func_level == '2008r2':
798             self.setvar("FUNCTION_LEVEL_INT", str(4))
799         elif func_level == '2003':
800             self.setvar("FUNCTION_LEVEL_INT", str(1))
801         else:
802             self.setvar("FUNCTION_LEVEL_INT", str(0))
803
804         child = self.open_telnet("${WIN_HOSTNAME}", "administrator", "${WIN_PASS}", set_ip=True, set_noexpire=True)
805
806         """This server must therefore not yet be a directory server, so we must promote it"""
807         child.sendline("copy /Y con answers.txt")
808         child.sendline('''
809 [DCInstall]
810 ; New forest promotion
811 ReplicaOrNewDomain=Domain
812 NewDomain=Forest
813 NewDomainDNSName=${WIN_REALM}
814 ForestLevel=${FUNCTION_LEVEL_INT}
815 DomainNetbiosName=${WIN_DOMAIN}
816 DomainLevel=${FUNCTION_LEVEL_INT}
817 InstallDNS=Yes
818 ConfirmGc=Yes
819 CreateDNSDelegation=No
820 DatabasePath="C:\Windows\NTDS"
821 LogPath="C:\Windows\NTDS"
822 SYSVOLPath="C:\Windows\SYSVOL"
823 ; Set SafeModeAdminPassword to the correct value prior to using the unattend file
824 SafeModeAdminPassword=${WIN_PASS}
825 ; Run-time flags (optional)
826 RebootOnCompletion=No
827 \1a
828 ''')
829         child.expect("copied.")
830         child.expect("C:")
831         child.expect("C:")
832         child.sendline("dcpromo /answer:answers.txt")
833         i = child.expect(["You must restart this computer", "failed", "Active Directory Domain Services was not installed", "C:"], timeout=240)
834         if i == 1 or i == 2:
835             raise Exception("dcpromo failed")
836         child.sendline("shutdown -r -t 0")
837         self.port_wait("${WIN_IP}", 139, wait_for_fail=True)
838         self.port_wait("${WIN_IP}", 139)
839         self.retry_cmd("host -t SRV _ldap._tcp.${WIN_REALM} ${WIN_IP}", ['has SRV record'], retries=60, delay=5 )
840
841
842     def start_winvm(self, vm):
843         '''start a Windows VM'''
844         self.setwinvars(vm)
845         
846         self.info("Joining a windows box to the domain")
847         self.vm_poweroff("${WIN_VM}", checkfail=False)
848         self.vm_restore("${WIN_VM}", "${WIN_SNAPSHOT}")
849
850     def run_winjoin(self, vm, domain, username="administrator", password="${PASSWORD1}"):
851         '''join a windows box to a domain'''
852         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True, set_noexpire=True)
853         retries = 5
854         while retries > 0:
855             child.sendline("ipconfig /flushdns")
856             child.expect("C:")
857             child.sendline("netdom join ${WIN_HOSTNAME} /Domain:%s /UserD:%s /PasswordD:%s" % (domain, username, password))
858             i = child.expect(["The command completed successfully", 
859                              "The specified domain either does not exist or could not be contacted."])
860             if i == 0:
861                 break
862             time.sleep(10)
863             retries -= 1
864
865         child.expect("C:")
866         child.sendline("shutdown /r -t 0")
867         self.wait_reboot()
868         child = self.open_telnet("${WIN_HOSTNAME}", "${WIN_USER}", "${WIN_PASS}", set_time=True, set_ip=True)
869         child.sendline("ipconfig /registerdns")
870         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")
871         child.expect("C:")
872
873
874     def test_remote_smbclient(self, vm, username="${WIN_USER}", password="${WIN_PASS}", args=""):
875         '''test smbclient against remote server'''
876         self.setwinvars(vm)
877         self.info('Testing smbclient')
878         self.chdir('${PREFIX}')
879         smbclient = self.getvar("smbclient")
880         self.cmd_contains("%s --version" % (smbclient), ["${SAMBA_VERSION}"])
881         self.retry_cmd('%s -L ${WIN_HOSTNAME} -U%s%%%s %s' % (smbclient, username, password, args), ["IPC"], retries=60, delay=5)
882
883     def test_net_use(self, vm, realm, domain, username, password):
884         self.setwinvars(vm)
885         self.info('Testing net use against Samba3 member')
886         child = self.open_telnet("${WIN_HOSTNAME}", "%s\\%s" % (domain, username), password)
887         child.sendline("net use t: \\\\${HOSTNAME}.%s\\test" % realm)
888         child.expect("The command completed successfully")
889
890
891     def setup(self, testname, subdir):
892         '''setup for main tests, parsing command line'''
893         self.parser.add_option("--conf", type='string', default='', help='config file')
894         self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
895         self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
896         self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
897         self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
898         self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
899         self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
900         self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
901         self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
902         self.parser.add_option("--use-ntvfs", action='store_true', default=False, help='use NTVFS for the fileserver')
903         self.parser.add_option("--dns-backend", type="choice",
904             choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
905             help="The DNS server backend. SAMBA_INTERNAL is the builtin name server, " \
906                  "BIND9_FLATFILE uses bind9 text database to store zone information, " \
907                  "BIND9_DLZ uses samba4 AD to store zone information (default), " \
908                  "NONE skips the DNS setup entirely (not recommended)",
909             default="BIND9_DLZ")
910
911         self.opts, self.args = self.parser.parse_args()
912
913         if not self.opts.conf:
914             print("Please specify a config file with --conf")
915             sys.exit(1)
916
917         # we don't need fsync safety in these tests
918         self.putenv('TDB_NO_FSYNC', '1')
919
920         self.load_config(self.opts.conf)
921
922         self.set_skip(self.opts.skip)
923         self.set_vms(self.opts.vms)
924
925         if self.opts.list:
926             self.list_steps_mode()
927
928         if self.opts.prefix:
929             self.setvar('PREFIX', self.opts.prefix)
930
931         if self.opts.sourcetree:
932             self.setvar('SOURCETREE', self.opts.sourcetree)
933
934         if self.opts.rebase:
935             self.info('rebasing')
936             self.chdir('${SOURCETREE}')
937             self.run_cmd('git pull --rebase')
938
939         if self.opts.clean:
940             self.info('cleaning')
941             self.chdir('${SOURCETREE}/' + subdir)
942             self.run_cmd('make clean')
943
944         if self.opts.use_ntvfs:
945             self.setvar('USE_NTVFS', "--use-ntvfs")
946         else:
947             self.setvar('USE_NTVFS', "")
948
949         self.setvar('NAMESERVER_BACKEND', self.opts.dns_backend)
950
951         if self.opts.dns_backend == 'SAMBA_INTERNAL':
952             self.setvar('ALLOW_DNS_UPDATES', '--option=allow dns updates = True')
953         else:
954             self.setvar('ALLOW_DNS_UPDATES', '')