wintest Remove the password expiry as the first step
[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 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 set_noexpire(self, child, username):
375         '''Ensure this user's password does not expire'''
376         child.sendline('wmic useraccount where name="%s" set PasswordExpires=FALSE' % username)
377         child.expect("update successful")
378         child.expect("C:")
379
380     def run_tlntadmn(self, child):
381         '''remove the annoying telnet restrictions'''
382         child.sendline('tlntadmn config maxconn=1024')
383         child.expect("The settings were successfully updated")
384         child.expect("C:")
385
386     def disable_firewall(self, child):
387         '''remove the annoying firewall'''
388         child.sendline('netsh advfirewall set allprofiles state off')
389         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
390         child.expect("C:")
391         if i == 1:
392             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
393             i = child.expect(["Ok", "The following command was not found"])
394             if i != 0:
395                 self.info("Firewall disable failed - ignoring")
396             child.expect("C:")
397  
398     def set_dns(self, child):
399         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
400         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
401         if i > 0:
402             return True
403         else:
404             return False
405
406     def set_ip(self, child):
407         """fix the IP address to the same value it had when we
408         connected, but don't use DHCP, and force the DNS server to our
409         DNS server.  This allows DNS updates to run"""
410         self.get_ipconfig(child)
411         if self.getvar("WIN_IPV4_ADDRESS") != self.getvar("WIN_IP"):
412             raise RuntimeError("ipconfig address %s != nmblookup address %s" % (self.getvar("WIN_IPV4_ADDRESS"),
413                                                                                 self.getvar("WIN_IP")))
414         child.sendline('netsh')
415         child.expect('netsh>')
416         child.sendline('offline')
417         child.expect('netsh>')
418         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
419         child.expect('netsh>')
420         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
421         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)
422         if i == 0:
423             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
424             child.expect('netsh>')
425         child.sendline('commit')
426         child.sendline('online')
427         child.sendline('exit')
428
429         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
430         return True
431
432
433     def resolve_ip(self, hostname, retries=60, delay=5):
434         '''resolve an IP given a hostname, assuming NBT'''
435         while retries > 0:
436             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
437             i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
438             if i == 0:
439                 return child.after
440             retries -= 1
441             time.sleep(delay)
442             self.info("retrying (retries=%u delay=%u)" % (retries, delay))
443         raise RuntimeError("Failed to resolve IP of %s" % hostname)
444
445
446     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
447                     disable_firewall=True, run_tlntadmn=True, set_noexpire=False):
448         '''open a telnet connection to a windows server, return the pexpect child'''
449         set_route = False
450         set_dns = False
451         if self.getvar('WIN_IP'):
452             ip = self.getvar('WIN_IP')
453         else:
454             ip = self.resolve_ip(hostname)
455             self.setvar('WIN_IP', ip)
456         while retries > 0:
457             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
458             i = child.expect(["Welcome to Microsoft Telnet Service",
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             child.expect("password:")
472             child.sendline(password)
473             i = child.expect(["C:",
474                               "Denying new connections due to the limit on number of connections",
475                               "No more connections are allowed to telnet server",
476                               "Unable to connect to remote host",
477                               "No route to host",
478                               "Connection refused",
479                               pexpect.EOF])
480             if i != 0:
481                 child.close()
482                 time.sleep(delay)
483                 retries -= 1
484                 self.info("retrying (retries=%u delay=%u)" % (retries, delay))
485                 continue
486             if set_dns:
487                 set_dns = False
488                 if self.set_dns(child):
489                     continue;
490             if set_route:
491                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
492                 child.expect("C:")
493                 set_route = False
494             if set_time:
495                 self.run_date_time(child, None)
496                 set_time = False
497             if run_tlntadmn:
498                 self.run_tlntadmn(child)
499                 run_tlntadmn = False
500             if set_noexpire:
501                 self.set_noexpire(child, username)
502                 set_noexpire = False
503             if disable_firewall:
504                 self.disable_firewall(child)
505                 disable_firewall = False
506             if set_ip:
507                 set_ip = False
508                 if self.set_ip(child):
509                     set_route = True
510                     set_dns = True
511                 continue
512             return child
513         raise RuntimeError("Failed to connect with telnet")
514
515     def kinit(self, username, password):
516         '''use kinit to setup a credentials cache'''
517         self.run_cmd("kdestroy")
518         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
519         username = self.substitute(username)
520         s = username.split('@')
521         if len(s) > 0:
522             s[1] = s[1].upper()
523         username = '@'.join(s)
524         child = self.pexpect_spawn('kinit ' + username)
525         child.expect("Password")
526         child.sendline(password)
527         child.expect(pexpect.EOF)
528         child.close()
529         if child.exitstatus != 0:
530             raise RuntimeError("kinit failed with status %d" % child.exitstatus)
531
532     def get_domains(self):
533         '''return a dictionary of DNS domains and IPs for named.conf'''
534         ret = {}
535         for v in self.vars:
536             if v[-6:] == "_REALM":
537                 base = v[:-6]
538                 if base + '_IP' in self.vars:
539                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
540         return ret
541
542     def wait_reboot(self, retries=3):
543         '''wait for a VM to reboot'''
544
545         # first wait for it to shutdown
546         self.port_wait("${WIN_IP}", 139, wait_for_fail=True, delay=6)
547
548         # now wait for it to come back. If it fails to come back
549         # then try resetting it
550         while retries > 0:
551             try:
552                 self.port_wait("${WIN_IP}", 139)
553                 return
554             except:
555                 retries -= 1
556                 self.vm_reset("${WIN_VM}")
557                 self.info("retrying reboot (retries=%u)" % retries)
558         raise RuntimeError(self.substitute("VM ${WIN_VM} failed to reboot"))
559
560     def get_vms(self):
561         '''return a dictionary of all the configured VM names'''
562         ret = []
563         for v in self.vars:
564             if v[-3:] == "_VM":
565                 ret.append(self.vars[v])
566         return ret
567
568     def setup(self, testname, subdir):
569         '''setup for main tests, parsing command line'''
570         self.parser.add_option("--conf", type='string', default='', help='config file')
571         self.parser.add_option("--skip", type='string', default='', help='list of steps to skip (comma separated)')
572         self.parser.add_option("--vms", type='string', default=None, help='list of VMs to use (comma separated)')
573         self.parser.add_option("--list", action='store_true', default=False, help='list the available steps')
574         self.parser.add_option("--rebase", action='store_true', default=False, help='do a git pull --rebase')
575         self.parser.add_option("--clean", action='store_true', default=False, help='clean the tree')
576         self.parser.add_option("--prefix", type='string', default=None, help='override install prefix')
577         self.parser.add_option("--sourcetree", type='string', default=None, help='override sourcetree location')
578         self.parser.add_option("--nocleanup", action='store_true', default=False, help='disable cleanup code')
579
580         self.opts, self.args = self.parser.parse_args()
581
582         if not self.opts.conf:
583             print("Please specify a config file with --conf")
584             sys.exit(1)
585
586         # we don't need fsync safety in these tests
587         self.putenv('TDB_NO_FSYNC', '1')
588
589         self.load_config(self.opts.conf)
590
591         self.set_skip(self.opts.skip)
592         self.set_vms(self.opts.vms)
593
594         if self.opts.list:
595             self.list_steps_mode()
596
597         if self.opts.prefix:
598             self.setvar('PREFIX', self.opts.prefix)
599
600         if self.opts.sourcetree:
601             self.setvar('SOURCETREE', self.opts.sourcetree)
602
603         if self.opts.rebase:
604             self.info('rebasing')
605             self.chdir('${SOURCETREE}')
606             self.run_cmd('git pull --rebase')
607
608         if self.opts.clean:
609             self.info('cleaning')
610             self.chdir('${SOURCETREE}/' + subdir)
611             self.run_cmd('make clean')