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