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