wintest: wait for the windows DC to register its DNS name
[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 sys, os, time, re
7
8 class wintest():
9     '''testing of Samba against windows VMs'''
10
11     def __init__(self):
12         self.vars = {}
13         self.list_mode = False
14         self.vms = None
15         os.putenv('PYTHONUNBUFFERED', '1')
16
17     def setvar(self, varname, value):
18         '''set a substitution variable'''
19         self.vars[varname] = value
20
21     def getvar(self, varname):
22         '''return a substitution variable'''
23         if not varname in self.vars:
24             return None
25         return self.vars[varname]
26
27     def setwinvars(self, vm, prefix='WIN'):
28         '''setup WIN_XX vars based on a vm name'''
29         for v in ['VM', 'HOSTNAME', 'USER', 'PASS', 'SNAPSHOT', 'BASEDN', 'REALM', 'DOMAIN', 'IP']:
30             vname = '%s_%s' % (vm, v)
31             if vname in self.vars:
32                 self.setvar("%s_%s" % (prefix,v), self.substitute("${%s}" % vname))
33             else:
34                 self.vars.pop("%s_%s" % (prefix,v), None)
35
36     def info(self, msg):
37         '''print some information'''
38         if not self.list_mode:
39             print(self.substitute(msg))
40
41     def load_config(self, fname):
42         '''load the config file'''
43         f = open(fname)
44         for line in f:
45             line = line.strip()
46             if len(line) == 0 or line[0] == '#':
47                 continue
48             colon = line.find(':')
49             if colon == -1:
50                 raise RuntimeError("Invalid config line '%s'" % line)
51             varname = line[0:colon].strip()
52             value   = line[colon+1:].strip()
53             self.setvar(varname, value)
54
55     def list_steps_mode(self):
56         '''put wintest in step listing mode'''
57         self.list_mode = True
58
59     def set_skip(self, skiplist):
60         '''set a list of tests to skip'''
61         self.skiplist = skiplist.split(',')
62
63     def set_vms(self, vms):
64         '''set a list of VMs to test'''
65         self.vms = vms.split(',')
66
67     def skip(self, step):
68         '''return True if we should skip a step'''
69         if self.list_mode:
70             print("\t%s" % step)
71             return True
72         return step in self.skiplist
73
74     def substitute(self, text):
75         """Substitute strings of the form ${NAME} in text, replacing
76         with substitutions from vars.
77         """
78         if isinstance(text, list):
79             ret = text[:]
80             for i in range(len(ret)):
81                 ret[i] = self.substitute(ret[i])
82             return ret
83
84         """We may have objects such as pexpect.EOF that are not strings"""
85         if not isinstance(text, str):
86             return text
87         while True:
88             var_start = text.find("${")
89             if var_start == -1:
90                 return text
91             var_end = text.find("}", var_start)
92             if var_end == -1:
93                 return text
94             var_name = text[var_start+2:var_end]
95             if not var_name in self.vars:
96                 raise RuntimeError("Unknown substitution variable ${%s}" % var_name)
97             text = text.replace("${%s}" % var_name, self.vars[var_name])
98         return text
99
100     def have_var(self, varname):
101         '''see if a variable has been set'''
102         return varname in self.vars
103
104     def have_vm(self, vmname):
105         '''see if a VM should be used'''
106         if not self.have_var(vmname + '_VM'):
107             return False
108         if self.vms is None:
109             return True
110         return vmname in self.vms
111
112     def putenv(self, key, value):
113         '''putenv with substitution'''
114         os.putenv(key, self.substitute(value))
115
116     def chdir(self, dir):
117         '''chdir with substitution'''
118         os.chdir(self.substitute(dir))
119
120     def del_files(self, dirs):
121         '''delete all files in the given directory'''
122         for d in dirs:
123             self.run_cmd("find %s -type f | xargs rm -f" % d)
124
125     def write_file(self, filename, text, mode='w'):
126         '''write to a file'''
127         f = open(self.substitute(filename), mode=mode)
128         f.write(self.substitute(text))
129         f.close()
130
131     def run_cmd(self, cmd, dir=".", show=None, output=False, checkfail=True):
132         '''run a command'''
133         cmd = self.substitute(cmd)
134         if isinstance(cmd, list):
135             self.info('$ ' + " ".join(cmd))
136         else:
137             self.info('$ ' + cmd)
138         if output:
139             return subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=dir).communicate()[0]
140         if isinstance(cmd, list):
141             shell=False
142         else:
143             shell=True
144         if checkfail:
145             return subprocess.check_call(cmd, shell=shell, cwd=dir)
146         else:
147             return subprocess.call(cmd, shell=shell, cwd=dir)
148
149
150     def run_child(self, cmd, dir="."):
151         '''create a child and return the Popen handle to it'''
152         cwd = os.getcwd()
153         cmd = self.substitute(cmd)
154         if isinstance(cmd, list):
155             self.info('$ ' + " ".join(cmd))
156         else:
157             self.info('$ ' + cmd)
158         if isinstance(cmd, list):
159             shell=False
160         else:
161             shell=True
162         os.chdir(dir)
163         ret = subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT)
164         os.chdir(cwd)
165         return ret
166
167     def cmd_output(self, cmd):
168         '''return output from and command'''
169         cmd = self.substitute(cmd)
170         return self.run_cmd(cmd, output=True)
171
172     def cmd_contains(self, cmd, contains, nomatch=False, ordered=False, regex=False,
173                      casefold=False):
174         '''check that command output contains the listed strings'''
175
176         if isinstance(contains, str):
177             contains = [contains]
178
179         out = self.cmd_output(cmd)
180         self.info(out)
181         for c in self.substitute(contains):
182             if regex:
183                 m = re.search(c, out)
184                 if m is None:
185                     start = -1
186                     end = -1
187                 else:
188                     start = m.start()
189                     end = m.end()
190             elif casefold:
191                 start = out.upper().find(c.upper())
192                 end = start + len(c)
193             else:
194                 start = out.find(c)
195                 end = start + len(c)
196             if nomatch:
197                 if start != -1:
198                     raise RuntimeError("Expected to not see %s in %s" % (c, cmd))
199             else:
200                 if start == -1:
201                     raise RuntimeError("Expected to see %s in %s" % (c, cmd))
202             if ordered and start != -1:
203                 out = out[end:]
204
205     def retry_cmd(self, cmd, contains, retries=30, delay=2, wait_for_fail=False,
206                   ordered=False, regex=False, casefold=False):
207         '''retry a command a number of times'''
208         while retries > 0:
209             try:
210                 self.cmd_contains(cmd, contains, nomatch=wait_for_fail,
211                                   ordered=ordered, regex=regex, casefold=casefold)
212                 return
213             except:
214                 time.sleep(delay)
215                 retries -= 1
216                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
217         raise RuntimeError("Failed to find %s" % contains)
218
219     def pexpect_spawn(self, cmd, timeout=60, crlf=True, casefold=True):
220         '''wrapper around pexpect spawn'''
221         cmd = self.substitute(cmd)
222         self.info("$ " + cmd)
223         ret = pexpect.spawn(cmd, logfile=sys.stdout, timeout=timeout)
224
225         def sendline_sub(line):
226             line = self.substitute(line)
227             if crlf:
228                 line = line.replace('\n', '\r\n') + '\r'
229             return ret.old_sendline(line)
230
231         def expect_sub(line, timeout=ret.timeout, casefold=casefold):
232             line = self.substitute(line)
233             if casefold:
234                 if isinstance(line, list):
235                     for i in range(len(line)):
236                         if isinstance(line[i], str):
237                             line[i] = '(?i)' + line[i]
238                 elif isinstance(line, str):
239                     line = '(?i)' + line
240             return ret.old_expect(line, timeout=timeout)
241
242         ret.old_sendline = ret.sendline
243         ret.sendline = sendline_sub
244         ret.old_expect = ret.expect
245         ret.expect = expect_sub
246
247         return ret
248
249     def get_nameserver(self):
250         '''Get the current nameserver from /etc/resolv.conf'''
251         child = self.pexpect_spawn('cat /etc/resolv.conf', crlf=False)
252         i = child.expect(['Generated by wintest', 'nameserver'])
253         if i == 0:
254             child.expect('your original resolv.conf')
255             child.expect('nameserver')
256         child.expect('\d+.\d+.\d+.\d+')
257         return child.after
258
259     def vm_poweroff(self, vmname, checkfail=True):
260         '''power off a VM'''
261         self.setvar('VMNAME', vmname)
262         self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
263
264     def vm_reset(self, vmname):
265         '''reset a VM'''
266         self.setvar('VMNAME', vmname)
267         self.run_cmd("${VM_RESET}")
268
269     def vm_restore(self, vmname, snapshot):
270         '''restore a VM'''
271         self.setvar('VMNAME', vmname)
272         self.setvar('SNAPSHOT', snapshot)
273         self.run_cmd("${VM_RESTORE}")
274
275     def ping_wait(self, hostname):
276         '''wait for a hostname to come up on the network'''
277         hostname = self.substitute(hostname)
278         loops=10
279         while loops > 0:
280             try:
281                 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
282                 break
283             except:
284                 loops = loops - 1
285         if loops == 0:
286             raise RuntimeError("Failed to ping %s" % hostname)
287         self.info("Host %s is up" % hostname)
288
289     def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
290         '''wait for a host to come up on the network'''
291         self.retry_cmd("nc -v -z -w 1 %s %u" % (hostname, port), ['succeeded'],
292                        retries=retries, delay=delay, wait_for_fail=wait_for_fail)
293
294     def run_net_time(self, child):
295         '''run net time on windows'''
296         child.sendline("net time \\\\${HOSTNAME} /set")
297         child.expect("Do you want to set the local computer")
298         child.sendline("Y")
299         child.expect("The command completed successfully")
300
301     def run_date_time(self, child, time_tuple=None):
302         '''run date and time on windows'''
303         if time_tuple is None:
304             time_tuple = time.localtime()
305         child.sendline("date")
306         child.expect("Enter the new date:")
307         i = child.expect(["dd-mm-yy", "mm-dd-yy"])
308         if i == 0:
309             child.sendline(time.strftime("%d-%m-%y", time_tuple))
310         else:
311             child.sendline(time.strftime("%m-%d-%y", time_tuple))
312         child.expect("C:")
313         child.sendline("time")
314         child.expect("Enter the new time:")
315         child.sendline(time.strftime("%H:%M:%S", time_tuple))
316         child.expect("C:")
317
318     def get_ipconfig(self, child):
319         '''get the IP configuration of the child'''
320         child.sendline("ipconfig /all")
321         child.expect('Ethernet adapter ')
322         child.expect("[\w\s]+")
323         self.setvar("WIN_NIC", child.after)
324         child.expect(['IPv4 Address', 'IP Address'])
325         child.expect('\d+.\d+.\d+.\d+')
326         self.setvar('WIN_IPV4_ADDRESS', child.after)
327         child.expect('Subnet Mask')
328         child.expect('\d+.\d+.\d+.\d+')
329         self.setvar('WIN_SUBNET_MASK', child.after)
330         child.expect('Default Gateway')
331         child.expect('\d+.\d+.\d+.\d+')
332         self.setvar('WIN_DEFAULT_GATEWAY', child.after)
333         child.expect("C:")
334
335     def run_tlntadmn(self, child):
336         '''remove the annoying telnet restrictions'''
337         child.sendline('tlntadmn config maxconn=1024')
338         child.expect("The settings were successfully updated")
339         child.expect("C:")
340
341     def disable_firewall(self, child):
342         '''remove the annoying firewall'''
343         child.sendline('netsh advfirewall set allprofiles state off')
344         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
345         child.expect("C:")
346         if i == 1:
347             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
348             i = child.expect(["Ok", "The following command was not found"])
349             if i != 0:
350                 self.info("Firewall disable failed - ignoring")
351             child.expect("C:")
352  
353     def set_dns(self, child):
354         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
355         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
356         if i > 0:
357             return True
358         else:
359             return False
360
361     def set_ip(self, child):
362         """fix the IP address to the same value it had when we
363         connected, but don't use DHCP, and force the DNS server to our
364         DNS server.  This allows DNS updates to run"""
365         self.get_ipconfig(child)
366         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
367             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
368                                                                                 self.getvar("WIN_IP")))
369         child.sendline('netsh')
370         child.expect('netsh>')
371         child.sendline('offline')
372         child.expect('netsh>')
373         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
374         child.expect('netsh>')
375         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
376         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)
377         if i == 0:
378             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
379             child.expect('netsh>')
380         child.sendline('commit')
381         child.sendline('online')
382         child.sendline('exit')
383
384         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
385         return True
386
387
388     def resolve_ip(self, hostname, retries=60, delay=5):
389         '''resolve an IP given a hostname, assuming NBT'''
390         while retries > 0:
391             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
392             i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
393             if i == 0:
394                 return child.after
395             retries -= 1
396             time.sleep(delay)
397             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
398         raise RuntimeError("Failed to resolve IP of %s" % hostname)
399
400
401     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
402                     disable_firewall=True, run_tlntadmn=True):
403         '''open a telnet connection to a windows server, return the pexpect child'''
404         set_route = False
405         set_dns = False
406         if self.getvar('WIN_IP'):
407             ip = self.getvar('WIN_IP')
408         else:
409             ip = self.resolve_ip(hostname)
410             self.setvar('WIN_IP', ip)
411         while retries > 0:
412             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
413             i = child.expect(["Welcome to Microsoft Telnet Service",
414                               "Denying new connections due to the limit on number of connections",
415                               "No more connections are allowed to telnet server",
416                               "Unable to connect to remote host",
417                               "No route to host",
418                               "Connection refused",
419                               pexpect.EOF])
420             if i != 0:
421                 child.close()
422                 time.sleep(delay)
423                 retries -= 1
424                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
425                 continue
426             child.expect("password:")
427             child.sendline(password)
428             i = child.expect(["C:",
429                               "Denying new connections due to the limit on number of connections",
430                               "No more connections are allowed to telnet server",
431                               "Unable to connect to remote host",
432                               "No route to host",
433                               "Connection refused",
434                               pexpect.EOF])
435             if i != 0:
436                 child.close()
437                 time.sleep(delay)
438                 retries -= 1
439                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
440                 continue
441             if set_dns:
442                 set_dns = False
443                 if self.set_dns(child):
444                     continue;
445             if set_route:
446                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
447                 child.expect("C:")
448                 set_route = False
449             if set_time:
450                 self.run_date_time(child, None)
451                 set_time = False
452             if run_tlntadmn:
453                 self.run_tlntadmn(child)
454                 run_tlntadmn = False
455             if disable_firewall:
456                 self.disable_firewall(child)
457                 disable_firewall = False
458             if set_ip:
459                 set_ip = False
460                 if self.set_ip(child):
461                     set_route = True
462                     set_dns = True
463                 continue
464             return child
465         raise RuntimeError("Failed to connect with telnet")
466
467     def kinit(self, username, password):
468         '''use kinit to setup a credentials cache'''
469         self.run_cmd("kdestroy")
470         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
471         username = self.substitute(username)
472         s = username.split('@')
473         if len(s) > 0:
474             s[1] = s[1].upper()
475         username = '@'.join(s)
476         child = self.pexpect_spawn('kinit ' + username)
477         child.expect("Password")
478         child.sendline(password)
479         child.expect(pexpect.EOF)
480         child.close()
481         if child.exitstatus != 0:
482             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
483
484     def get_domains(self):
485         '''return a dictionary of DNS domains and IPs for named.conf'''
486         ret = {}
487         for v in self.vars:
488             if v[-6:] == "_REALM":
489                 base = v[:-6]
490                 if base + '_IP' in self.vars:
491                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
492         return ret
493
494     def wait_reboot(self, retries=3):
495         '''wait for a VM to reboot'''
496
497         # first wait for it to shutdown
498         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
499
500         # now wait for it to come back. If it fails to come back
501         # then try resetting it
502         while retries > 0:
503             try:
504                 self.port_wait("${WIN_IP}", 139)
505                 return
506             except:
507                 retries -= 1
508                 self.vm_reset("${WIN_VM}")
509                 self.info("retrying reboot (retries=%u)" % retries)
510         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))