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