3 '''automated testing library for testing Samba against windows'''
5 import pexpect
, subprocess
7 import sys
, os
, time
, re
10 '''testing of Samba against windows VMs'''
14 self
.list_mode
= False
16 os
.putenv('PYTHONUNBUFFERED', '1')
17 self
.parser
= optparse
.OptionParser("wintest")
19 def setvar(self
, varname
, value
):
20 '''set a substitution variable'''
21 self
.vars[varname
] = value
23 def getvar(self
, varname
):
24 '''return a substitution variable'''
25 if not varname
in self
.vars:
27 return self
.vars[varname
]
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
))
36 self
.vars.pop("%s_%s" % (prefix
,v
), None)
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="))
45 '''print some information'''
46 if not self
.list_mode
:
47 print(self
.substitute(msg
))
49 def load_config(self
, fname
):
50 '''load the config file'''
54 if len(line
) == 0 or line
[0] == '#':
56 colon
= line
.find(':')
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
)
63 def list_steps_mode(self
):
64 '''put wintest in step listing mode'''
67 def set_skip(self
, skiplist
):
68 '''set a list of tests to skip'''
69 self
.skiplist
= skiplist
.split(',')
71 def set_vms(self
, vms
):
72 '''set a list of VMs to test'''
74 self
.vms
= vms
.split(',')
77 '''return True if we should skip a step'''
81 return step
in self
.skiplist
83 def substitute(self
, text
):
84 """Substitute strings of the form ${NAME} in text, replacing
85 with substitutions from vars.
87 if isinstance(text
, list):
89 for i
in range(len(ret
)):
90 ret
[i
] = self
.substitute(ret
[i
])
93 """We may have objects such as pexpect.EOF that are not strings"""
94 if not isinstance(text
, str):
97 var_start
= text
.find("${")
100 var_end
= text
.find("}", var_start
)
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
])
109 def have_var(self
, varname
):
110 '''see if a variable has been set'''
111 return varname
in self
.vars
113 def have_vm(self
, vmname
):
114 '''see if a VM should be used'''
115 if not self
.have_var(vmname
+ '_VM'):
119 return vmname
in self
.vms
121 def putenv(self
, key
, value
):
122 '''putenv with substitution'''
123 os
.putenv(key
, self
.substitute(value
))
125 def chdir(self
, dir):
126 '''chdir with substitution'''
127 os
.chdir(self
.substitute(dir))
129 def del_files(self
, dirs
):
130 '''delete all files in the given directory'''
132 self
.run_cmd("find %s -type f | xargs rm -f" % d
)
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
))
140 def run_cmd(self
, cmd
, dir=".", show
=None, output
=False, checkfail
=True):
142 cmd
= self
.substitute(cmd
)
143 if isinstance(cmd
, list):
144 self
.info('$ ' + " ".join(cmd
))
146 self
.info('$ ' + cmd
)
148 return subprocess
.Popen([cmd
], shell
=True, stdout
=subprocess
.PIPE
, stderr
=subprocess
.STDOUT
, cwd
=dir).communicate()[0]
149 if isinstance(cmd
, list):
154 return subprocess
.check_call(cmd
, shell
=shell
, cwd
=dir)
156 return subprocess
.call(cmd
, shell
=shell
, cwd
=dir)
159 def run_child(self
, cmd
, dir="."):
160 '''create a child and return the Popen handle to it'''
162 cmd
= self
.substitute(cmd
)
163 if isinstance(cmd
, list):
164 self
.info('$ ' + " ".join(cmd
))
166 self
.info('$ ' + cmd
)
167 if isinstance(cmd
, list):
172 ret
= subprocess
.Popen(cmd
, shell
=shell
, stderr
=subprocess
.STDOUT
)
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)
181 def cmd_contains(self
, cmd
, contains
, nomatch
=False, ordered
=False, regex
=False,
183 '''check that command output contains the listed strings'''
185 if isinstance(contains
, str):
186 contains
= [contains
]
188 out
= self
.cmd_output(cmd
)
190 for c
in self
.substitute(contains
):
195 m
= re
.search(c
, out
)
203 start
= out
.upper().find(c
.upper())
210 raise RuntimeError("Expected to not see %s in %s" % (c
, cmd
))
213 raise RuntimeError("Expected to see %s in %s" % (c
, cmd
))
214 if ordered
and start
!= -1:
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'''
222 self
.cmd_contains(cmd
, contains
, nomatch
=wait_for_fail
,
223 ordered
=ordered
, regex
=regex
, casefold
=casefold
)
228 self
.info("retrying (retries=%u delay=%u)" % (retries
, delay
))
229 raise RuntimeError("Failed to find %s" % contains
)
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
)
237 def sendline_sub(line
):
238 line
= self
.substitute(line
)
240 line
= line
.replace('\n', '\r\n') + '\r'
241 return ret
.old_sendline(line
)
243 def expect_sub(line
, timeout
=ret
.timeout
, casefold
=casefold
):
244 line
= self
.substitute(line
)
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):
252 return ret
.old_expect(line
, timeout
=timeout
)
254 ret
.old_sendline
= ret
.sendline
255 ret
.sendline
= sendline_sub
256 ret
.old_expect
= ret
.expect
257 ret
.expect
= expect_sub
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'])
266 child
.expect('your original resolv.conf')
267 child
.expect('nameserver')
268 child
.expect('\d+.\d+.\d+.\d+')
271 def vm_poweroff(self
, vmname
, checkfail
=True):
273 self
.setvar('VMNAME', vmname
)
274 self
.run_cmd("${VM_POWEROFF}", checkfail
=checkfail
)
276 def vm_reset(self
, vmname
):
278 self
.setvar('VMNAME', vmname
)
279 self
.run_cmd("${VM_RESET}")
281 def vm_restore(self
, vmname
, snapshot
):
283 self
.setvar('VMNAME', vmname
)
284 self
.setvar('SNAPSHOT', snapshot
)
285 self
.run_cmd("${VM_RESTORE}")
287 def ping_wait(self
, hostname
):
288 '''wait for a hostname to come up on the network'''
289 hostname
= self
.substitute(hostname
)
293 self
.run_cmd("ping -c 1 -w 10 %s" % hostname
)
298 raise RuntimeError("Failed to ping %s" % hostname
)
299 self
.info("Host %s is up" % hostname
)
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
)
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")
311 child
.expect("The command completed successfully")
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"])
321 child
.sendline(time
.strftime("%d-%m-%y", time_tuple
))
323 child
.sendline(time
.strftime("%m-%d-%y", time_tuple
))
325 child
.sendline("time")
326 child
.expect("Enter the new time:")
327 child
.sendline(time
.strftime("%H:%M:%S", time_tuple
))
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
)
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",
353 "passed test Replications"])
358 child
.sendline("net config Workstation")
359 child
.expect("Workstation domain")
360 child
.expect('[\S]+')
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'''
366 if domain
.upper() == self
.getvar("WIN_DOMAIN").upper():
369 child
.expect('[\S]+')
370 hostname
= child
.after
371 if hostname
.upper() == self
.getvar("WIN_HOSTNAME").upper():
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")
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"])
386 child
.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
387 i
= child
.expect(["Ok", "The following command was not found"])
389 self
.info("Firewall disable failed - ignoring")
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)
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)
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')
423 child
.expect([pexpect
.EOF
, pexpect
.TIMEOUT
], timeout
=5)
427 def resolve_ip(self
, hostname
, retries
=60, delay
=5):
428 '''resolve an IP given a hostname, assuming NBT'''
430 child
= self
.pexpect_spawn("bin/nmblookup %s" % hostname
)
431 i
= child
.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
436 self
.info("retrying (retries=%u delay=%u)" % (retries
, delay
))
437 raise RuntimeError("Failed to resolve IP of %s" % hostname
)
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'''
445 if self
.getvar('WIN_IP'):
446 ip
= self
.getvar('WIN_IP')
448 ip
= self
.resolve_ip(hostname
)
449 self
.setvar('WIN_IP', ip
)
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",
457 "Connection refused",
463 self
.info("retrying (retries=%u delay=%u)" % (retries
, delay
))
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",
472 "Connection refused",
478 self
.info("retrying (retries=%u delay=%u)" % (retries
, delay
))
482 if self
.set_dns(child
):
485 child
.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
489 self
.run_date_time(child
, None)
492 self
.run_tlntadmn(child
)
495 self
.disable_firewall(child
)
496 disable_firewall
= False
499 if self
.set_ip(child
):
504 raise RuntimeError("Failed to connect with telnet")
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('@')
514 username
= '@'.join(s
)
515 child
= self
.pexpect_spawn('kinit ' + username
)
516 child
.expect("Password")
517 child
.sendline(password
)
518 child
.expect(pexpect
.EOF
)
520 if child
.exitstatus
!= 0:
521 raise RuntimeError("kinit failed with status %d" % child
.exitstatus
)
523 def get_domains(self
):
524 '''return a dictionary of DNS domains and IPs for named.conf'''
527 if v
[-6:] == "_REALM":
529 if base
+ '_IP' in self
.vars:
530 ret
[self
.vars[base
+ '_REALM']] = self
.vars[base
+ '_IP']
533 def wait_reboot(self
, retries
=3):
534 '''wait for a VM to reboot'''
536 # first wait for it to shutdown
537 self
.port_wait("${WIN_IP}", 139, wait_for_fail
=True, delay
=6)
539 # now wait for it to come back. If it fails to come back
540 # then try resetting it
543 self
.port_wait("${WIN_IP}", 139)
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"))
552 '''return a dictionary of all the configured VM names'''
556 ret
.append(self
.vars[v
])
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')
571 self
.opts
, self
.args
= self
.parser
.parse_args()
573 if not self
.opts
.conf
:
574 print("Please specify a config file with --conf")
577 # we don't need fsync safety in these tests
578 self
.putenv('TDB_NO_FSYNC', '1')
580 self
.load_config(self
.opts
.conf
)
582 self
.set_skip(self
.opts
.skip
)
583 self
.set_vms(self
.opts
.vms
)
586 self
.list_steps_mode()
589 self
.setvar('PREFIX', self
.opts
.prefix
)
591 if self
.opts
.sourcetree
:
592 self
.setvar('SOURCETREE', self
.opts
.sourcetree
)
595 self
.info('rebasing')
596 self
.chdir('${SOURCETREE}')
597 self
.run_cmd('git pull --rebase')
600 self
.info('cleaning')
601 self
.chdir('${SOURCETREE}/' + subdir
)
602 self
.run_cmd('make clean')