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