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