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