wintest Add a function to shut down all the managed VMs at the start
[metze/samba/wip.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 get_is_dc(self, child):
336         child.sendline("dcdiag")
337         i = child.expect(["is not a Directory Server", "Home Server = "])
338         if i == 0:
339             return False
340         child.expect('[\S]+')
341         hostname = child.after
342         if hostname.upper() == self.getvar("WIN_HOSTNAME").upper:
343             return True
344
345     def run_tlntadmn(self, child):
346         '''remove the annoying telnet restrictions'''
347         child.sendline('tlntadmn config maxconn=1024')
348         child.expect("The settings were successfully updated")
349         child.expect("C:")
350
351     def disable_firewall(self, child):
352         '''remove the annoying firewall'''
353         child.sendline('netsh advfirewall set allprofiles state off')
354         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
355         child.expect("C:")
356         if i == 1:
357             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
358             i = child.expect(["Ok", "The following command was not found"])
359             if i != 0:
360                 self.info("Firewall disable failed - ignoring")
361             child.expect("C:")
362  
363     def set_dns(self, child):
364         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
365         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
366         if i > 0:
367             return True
368         else:
369             return False
370
371     def set_ip(self, child):
372         """fix the IP address to the same value it had when we
373         connected, but don't use DHCP, and force the DNS server to our
374         DNS server.  This allows DNS updates to run"""
375         self.get_ipconfig(child)
376         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
377             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
378                                                                                 self.getvar("WIN_IP")))
379         child.sendline('netsh')
380         child.expect('netsh>')
381         child.sendline('offline')
382         child.expect('netsh>')
383         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
384         child.expect('netsh>')
385         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
386         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)
387         if i == 0:
388             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
389             child.expect('netsh>')
390         child.sendline('commit')
391         child.sendline('online')
392         child.sendline('exit')
393
394         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
395         return True
396
397
398     def resolve_ip(self, hostname, retries=60, delay=5):
399         '''resolve an IP given a hostname, assuming NBT'''
400         while retries > 0:
401             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
402             i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
403             if i == 0:
404                 return child.after
405             retries -= 1
406             time.sleep(delay)
407             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
408         raise RuntimeError("Failed to resolve IP of %s" % hostname)
409
410
411     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
412                     disable_firewall=True, run_tlntadmn=True):
413         '''open a telnet connection to a windows server, return the pexpect child'''
414         set_route = False
415         set_dns = False
416         if self.getvar('WIN_IP'):
417             ip = self.getvar('WIN_IP')
418         else:
419             ip = self.resolve_ip(hostname)
420             self.setvar('WIN_IP', ip)
421         while retries > 0:
422             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
423             i = child.expect(["Welcome to Microsoft Telnet Service",
424                               "Denying new connections due to the limit on number of connections",
425                               "No more connections are allowed to telnet server",
426                               "Unable to connect to remote host",
427                               "No route to host",
428                               "Connection refused",
429                               pexpect.EOF])
430             if i != 0:
431                 child.close()
432                 time.sleep(delay)
433                 retries -= 1
434                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
435                 continue
436             child.expect("password:")
437             child.sendline(password)
438             i = child.expect(["C:",
439                               "Denying new connections due to the limit on number of connections",
440                               "No more connections are allowed to telnet server",
441                               "Unable to connect to remote host",
442                               "No route to host",
443                               "Connection refused",
444                               pexpect.EOF])
445             if i != 0:
446                 child.close()
447                 time.sleep(delay)
448                 retries -= 1
449                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
450                 continue
451             if set_dns:
452                 set_dns = False
453                 if self.set_dns(child):
454                     continue;
455             if set_route:
456                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
457                 child.expect("C:")
458                 set_route = False
459             if set_time:
460                 self.run_date_time(child, None)
461                 set_time = False
462             if run_tlntadmn:
463                 self.run_tlntadmn(child)
464                 run_tlntadmn = False
465             if disable_firewall:
466                 self.disable_firewall(child)
467                 disable_firewall = False
468             if set_ip:
469                 set_ip = False
470                 if self.set_ip(child):
471                     set_route = True
472                     set_dns = True
473                 continue
474             return child
475         raise RuntimeError("Failed to connect with telnet")
476
477     def kinit(self, username, password):
478         '''use kinit to setup a credentials cache'''
479         self.run_cmd("kdestroy")
480         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
481         username = self.substitute(username)
482         s = username.split('@')
483         if len(s) > 0:
484             s[1] = s[1].upper()
485         username = '@'.join(s)
486         child = self.pexpect_spawn('kinit ' + username)
487         child.expect("Password")
488         child.sendline(password)
489         child.expect(pexpect.EOF)
490         child.close()
491         if child.exitstatus != 0:
492             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
493
494     def get_domains(self):
495         '''return a dictionary of DNS domains and IPs for named.conf'''
496         ret = {}
497         for v in self.vars:
498             if v[-6:] == "_REALM":
499                 base = v[:-6]
500                 if base + '_IP' in self.vars:
501                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
502         return ret
503
504     def wait_reboot(self, retries=3):
505         '''wait for a VM to reboot'''
506
507         # first wait for it to shutdown
508         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
509
510         # now wait for it to come back. If it fails to come back
511         # then try resetting it
512         while retries > 0:
513             try:
514                 self.port_wait("${WIN_IP}", 139)
515                 return
516             except:
517                 retries -= 1
518                 self.vm_reset("${WIN_VM}")
519                 self.info("retrying reboot (retries=%u)" % retries)
520         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
521
522     def get_vms(self):
523         '''return a dictionary of all the configured VM names'''
524         ret = []
525         for v in self.vars:
526             if v[-3:] == "_VM":
527                 ret.append(self.vars[v])
528         return ret