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