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