wintest: added --vms option
[mat/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):
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).replace('\n', '\r\n')
227             return ret.old_sendline(line + '\r')
228
229         def expect_sub(line, timeout=ret.timeout):
230             line = self.substitute(line)
231             return ret.old_expect(line, timeout=timeout)
232
233         if crlf:
234             ret.old_sendline = ret.sendline
235             ret.sendline = sendline_sub
236         ret.old_expect = ret.expect
237         ret.expect = expect_sub
238
239         return ret
240
241     def get_nameserver(self):
242         '''Get the current nameserver from /etc/resolv.conf'''
243         child = self.pexpect_spawn('cat /etc/resolv.conf', crlf=False)
244         i = child.expect(['Generated by wintest', 'nameserver'])
245         if i == 0:
246             child.expect('your original resolv.conf')
247             child.expect('nameserver')
248         child.expect('\d+.\d+.\d+.\d+')
249         return child.after
250
251     def vm_poweroff(self, vmname, checkfail=True):
252         '''power off a VM'''
253         self.setvar('VMNAME', vmname)
254         self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
255
256     def vm_reset(self, vmname):
257         '''reset a VM'''
258         self.setvar('VMNAME', vmname)
259         self.run_cmd("${VM_RESET}")
260
261     def vm_restore(self, vmname, snapshot):
262         '''restore a VM'''
263         self.setvar('VMNAME', vmname)
264         self.setvar('SNAPSHOT', snapshot)
265         self.run_cmd("${VM_RESTORE}")
266
267     def ping_wait(self, hostname):
268         '''wait for a hostname to come up on the network'''
269         hostname = self.substitute(hostname)
270         loops=10
271         while loops > 0:
272             try:
273                 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
274                 break
275             except:
276                 loops = loops - 1
277         if loops == 0:
278             raise RuntimeError("Failed to ping %s" % hostname)
279         self.info("Host %s is up" % hostname)
280
281     def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
282         '''wait for a host to come up on the network'''
283         self.retry_cmd("nc -v -z -w 1 %s %u" % (hostname, port), ['succeeded'],
284                        retries=retries, delay=delay, wait_for_fail=wait_for_fail)
285
286     def run_net_time(self, child):
287         '''run net time on windows'''
288         child.sendline("net time \\\\${HOSTNAME} /set")
289         child.expect("Do you want to set the local computer")
290         child.sendline("Y")
291         child.expect("The command completed successfully")
292
293     def run_date_time(self, child, time_tuple=None):
294         '''run date and time on windows'''
295         if time_tuple is None:
296             time_tuple = time.localtime()
297         child.sendline("date")
298         child.expect("Enter the new date:")
299         i = child.expect(["dd-mm-yy", "mm-dd-yy"])
300         if i == 0:
301             child.sendline(time.strftime("%d-%m-%y", time_tuple))
302         else:
303             child.sendline(time.strftime("%m-%d-%y", time_tuple))
304         child.expect("C:")
305         child.sendline("time")
306         child.expect("Enter the new time:")
307         child.sendline(time.strftime("%H:%M:%S", time_tuple))
308         child.expect("C:")
309
310     def get_ipconfig(self, child):
311         '''get the IP configuration of the child'''
312         child.sendline("ipconfig /all")
313         child.expect('Ethernet adapter ')
314         child.expect("[\w\s]+")
315         self.setvar("WIN_NIC", child.after)
316         child.expect(['IPv4 Address', 'IP Address'])
317         child.expect('\d+.\d+.\d+.\d+')
318         self.setvar('WIN_IPV4_ADDRESS', child.after)
319         child.expect('Subnet Mask')
320         child.expect('\d+.\d+.\d+.\d+')
321         self.setvar('WIN_SUBNET_MASK', child.after)
322         child.expect('Default Gateway')
323         child.expect('\d+.\d+.\d+.\d+')
324         self.setvar('WIN_DEFAULT_GATEWAY', child.after)
325         child.expect("C:")
326
327     def run_tlntadmn(self, child):
328         '''remove the annoying telnet restrictions'''
329         child.sendline('tlntadmn config maxconn=1024')
330         child.expect("The settings were successfully updated")
331         child.expect("C:")
332
333     def disable_firewall(self, child):
334         '''remove the annoying firewall'''
335         child.sendline('netsh advfirewall set allprofiles state off')
336         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
337         child.expect("C:")
338         if i == 1:
339             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
340             i = child.expect(["Ok", "The following command was not found"])
341             if i != 0:
342                 self.info("Firewall disable failed - ignoring")
343             child.expect("C:")
344  
345     def set_dns(self, child):
346         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
347         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
348         if i > 0:
349             return True
350         else:
351             return False
352
353     def set_ip(self, child):
354         """fix the IP address to the same value it had when we
355         connected, but don't use DHCP, and force the DNS server to our
356         DNS server.  This allows DNS updates to run"""
357         self.get_ipconfig(child)
358         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
359             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
360                                                                                 self.getvar("WIN_IP")))
361         child.sendline('netsh')
362         child.expect('netsh>')
363         child.sendline('offline')
364         child.expect('netsh>')
365         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
366         child.expect('netsh>')
367         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
368         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)
369         if i == 0:
370             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
371             child.expect('netsh>')
372         child.sendline('commit')
373         child.sendline('online')
374         child.sendline('exit')
375
376         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
377         return True
378
379
380     def resolve_ip(self, hostname, retries=60, delay=5):
381         '''resolve an IP given a hostname, assuming NBT'''
382         while retries > 0:
383             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
384             i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
385             if i == 0:
386                 return child.after
387             retries -= 1
388             time.sleep(delay)
389             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
390         raise RuntimeError("Failed to resolve IP of %s" % hostname)
391
392
393     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
394                     disable_firewall=True, run_tlntadmn=True):
395         '''open a telnet connection to a windows server, return the pexpect child'''
396         set_route = False
397         set_dns = False
398         if self.getvar('WIN_IP'):
399             ip = self.getvar('WIN_IP')
400         else:
401             ip = self.resolve_ip(hostname)
402             self.setvar('WIN_IP', ip)
403         while retries > 0:
404             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
405             i = child.expect(["Welcome to Microsoft Telnet Service",
406                               "Denying new connections due to the limit on number of connections",
407                               "No more connections are allowed to telnet server",
408                               "Unable to connect to remote host",
409                               "No route to host",
410                               "Connection refused",
411                               pexpect.EOF])
412             if i != 0:
413                 child.close()
414                 time.sleep(delay)
415                 retries -= 1
416                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
417                 continue
418             child.expect("password:")
419             child.sendline(password)
420             i = child.expect(["C:",
421                               "Denying new connections due to the limit on number of connections",
422                               "No more connections are allowed to telnet server",
423                               "Unable to connect to remote host",
424                               "No route to host",
425                               "Connection refused",
426                               pexpect.EOF])
427             if i != 0:
428                 child.close()
429                 time.sleep(delay)
430                 retries -= 1
431                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
432                 continue
433             if set_dns:
434                 set_dns = False
435                 if self.set_dns(child):
436                     continue;
437             if set_route:
438                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
439                 child.expect("C:")
440                 set_route = False
441             if set_time:
442                 self.run_date_time(child, None)
443                 set_time = False
444             if run_tlntadmn:
445                 self.run_tlntadmn(child)
446                 run_tlntadmn = False
447             if disable_firewall:
448                 self.disable_firewall(child)
449                 disable_firewall = False
450             if set_ip:
451                 set_ip = False
452                 if self.set_ip(child):
453                     set_route = True
454                     set_dns = True
455                 continue
456             return child
457         raise RuntimeError("Failed to connect with telnet")
458
459     def kinit(self, username, password):
460         '''use kinit to setup a credentials cache'''
461         self.run_cmd("kdestroy")
462         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
463         username = self.substitute(username)
464         s = username.split('@')
465         if len(s) > 0:
466             s[1] = s[1].upper()
467         username = '@'.join(s)
468         child = self.pexpect_spawn('kinit ' + username)
469         child.expect("Password")
470         child.sendline(password)
471         child.expect(pexpect.EOF)
472         child.close()
473         if child.exitstatus != 0:
474             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
475
476     def get_domains(self):
477         '''return a dictionary of DNS domains and IPs for named.conf'''
478         ret = {}
479         for v in self.vars:
480             if v[-6:] == "_REALM":
481                 base = v[:-6]
482                 if base + '_IP' in self.vars:
483                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
484         return ret
485
486     def wait_reboot(self, retries=3):
487         '''wait for a VM to reboot'''
488
489         # first wait for it to shutdown
490         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
491
492         # now wait for it to come back. If it fails to come back
493         # then try resetting it
494         while retries > 0:
495             try:
496                 self.port_wait("${WIN_IP}", 139)
497                 return
498             except:
499                 retries -= 1
500                 self.vm_reset("${WIN_VM}")
501                 self.info("retrying reboot (retries=%u)" % retries)
502         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))