wintest: fixed interface handling and DNS forwarding
[garming/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         os.putenv('PYTHONUNBUFFERED', '1')
15
16     def setvar(self, varname, value):
17         '''set a substitution variable'''
18         self.vars[varname] = value
19
20     def getvar(self, varname):
21         '''return a substitution variable'''
22         if not varname in self.vars:
23             return None
24         return self.vars[varname]
25
26     def setwinvars(self, vm, prefix='WIN'):
27         '''setup WIN_XX vars based on a vm name'''
28         for v in ['VM', 'HOSTNAME', 'USER', 'PASS', 'SNAPSHOT', 'BASEDN', 'REALM', 'DOMAIN', 'IP']:
29             vname = '%s_%s' % (vm, v)
30             if vname in self.vars:
31                 self.setvar("%s_%s" % (prefix,v), self.substitute("${%s}" % vname))
32             else:
33                 self.vars.pop("%s_%s" % (prefix,v), None)
34
35     def info(self, msg):
36         '''print some information'''
37         if not self.list_mode:
38             print(self.substitute(msg))
39
40     def load_config(self, fname):
41         '''load the config file'''
42         f = open(fname)
43         for line in f:
44             line = line.strip()
45             if len(line) == 0 or line[0] == '#':
46                 continue
47             colon = line.find(':')
48             if colon == -1:
49                 raise RuntimeError("Invalid config line '%s'" % line)
50             varname = line[0:colon].strip()
51             value   = line[colon+1:].strip()
52             self.setvar(varname, value)
53
54     def list_steps_mode(self):
55         '''put wintest in step listing mode'''
56         self.list_mode = True
57
58     def set_skip(self, skiplist):
59         '''set a list of tests to skip'''
60         self.skiplist = skiplist.split(',')
61
62     def skip(self, step):
63         '''return True if we should skip a step'''
64         if self.list_mode:
65             print("\t%s" % step)
66             return True
67         return step in self.skiplist
68
69     def substitute(self, text):
70         """Substitute strings of the form ${NAME} in text, replacing
71         with substitutions from vars.
72         """
73         if isinstance(text, list):
74             ret = text[:]
75             for i in range(len(ret)):
76                 ret[i] = self.substitute(ret[i])
77             return ret
78
79         """We may have objects such as pexpect.EOF that are not strings"""
80         if not isinstance(text, str):
81             return text
82         while True:
83             var_start = text.find("${")
84             if var_start == -1:
85                 return text
86             var_end = text.find("}", var_start)
87             if var_end == -1:
88                 return text
89             var_name = text[var_start+2:var_end]
90             if not var_name in self.vars:
91                 raise RuntimeError("Unknown substitution variable ${%s}" % var_name)
92             text = text.replace("${%s}" % var_name, self.vars[var_name])
93         return text
94
95     def have_var(self, varname):
96         '''see if a variable has been set'''
97         return varname in self.vars
98
99
100     def putenv(self, key, value):
101         '''putenv with substitution'''
102         os.putenv(key, self.substitute(value))
103
104     def chdir(self, dir):
105         '''chdir with substitution'''
106         os.chdir(self.substitute(dir))
107
108     def del_files(self, dirs):
109         '''delete all files in the given directory'''
110         for d in dirs:
111             self.run_cmd("find %s -type f | xargs rm -f" % d)
112
113     def write_file(self, filename, text, mode='w'):
114         '''write to a file'''
115         f = open(self.substitute(filename), mode=mode)
116         f.write(self.substitute(text))
117         f.close()
118
119     def run_cmd(self, cmd, dir=".", show=None, output=False, checkfail=True):
120         cmd = self.substitute(cmd)
121         if isinstance(cmd, list):
122             self.info('$ ' + " ".join(cmd))
123         else:
124             self.info('$ ' + cmd)
125         if output:
126             return subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=dir).communicate()[0]
127         if isinstance(cmd, list):
128             shell=False
129         else:
130             shell=True
131         if checkfail:
132             return subprocess.check_call(cmd, shell=shell, cwd=dir)
133         else:
134             return subprocess.call(cmd, shell=shell, cwd=dir)
135
136     def run_child(self, cmd, dir="."):
137         cwd = os.getcwd()
138         cmd = self.substitute(cmd)
139         if isinstance(cmd, list):
140             self.info('$ ' + " ".join(cmd))
141         else:
142             self.info('$ ' + cmd)
143         if isinstance(cmd, list):
144             shell=False
145         else:
146             shell=True
147         os.chdir(dir)
148         ret = subprocess.Popen(cmd, shell=shell)
149         os.chdir(cwd)
150         return ret
151
152     def cmd_output(self, cmd):
153         '''return output from and command'''
154         cmd = self.substitute(cmd)
155         return self.run_cmd(cmd, output=True)
156
157     def cmd_contains(self, cmd, contains, nomatch=False, ordered=False, regex=False,
158                      casefold=False):
159         '''check that command output contains the listed strings'''
160
161         if isinstance(contains, str):
162             contains = [contains]
163
164         out = self.cmd_output(cmd)
165         self.info(out)
166         for c in self.substitute(contains):
167             if regex:
168                 m = re.search(c, out)
169                 if m is None:
170                     start = -1
171                     end = -1
172                 else:
173                     start = m.start()
174                     end = m.end()
175             elif casefold:
176                 start = out.upper().find(c.upper())
177                 end = start + len(c)
178             else:
179                 start = out.find(c)
180                 end = start + len(c)
181             if nomatch:
182                 if start != -1:
183                     raise RuntimeError("Expected to not see %s in %s" % (c, cmd))
184             else:
185                 if start == -1:
186                     raise RuntimeError("Expected to see %s in %s" % (c, cmd))
187             if ordered and start != -1:
188                 out = out[end:]
189
190     def retry_cmd(self, cmd, contains, retries=30, delay=2, wait_for_fail=False,
191                   ordered=False, regex=False, casefold=False):
192         '''retry a command a number of times'''
193         while retries > 0:
194             try:
195                 self.cmd_contains(cmd, contains, nomatch=wait_for_fail,
196                                   ordered=ordered, regex=regex, casefold=casefold)
197                 return
198             except:
199                 time.sleep(delay)
200                 retries = retries - 1
201         raise RuntimeError("Failed to find %s" % contains)
202
203     def pexpect_spawn(self, cmd, timeout=60):
204         '''wrapper around pexpect spawn'''
205         cmd = self.substitute(cmd)
206         self.info("$ " + cmd)
207         ret = pexpect.spawn(cmd, logfile=sys.stdout, timeout=timeout)
208
209         def sendline_sub(line):
210             line = self.substitute(line).replace('\n', '\r\n')
211             return ret.old_sendline(line + '\r')
212
213         def expect_sub(line, timeout=ret.timeout):
214             line = self.substitute(line)
215             return ret.old_expect(line, timeout=timeout)
216
217         ret.old_sendline = ret.sendline
218         ret.sendline = sendline_sub
219         ret.old_expect = ret.expect
220         ret.expect = expect_sub
221
222         return ret
223
224     def get_nameserver(self):
225         '''Get the current nameserver from /etc/resolv.conf'''
226         child = self.pexpect_spawn('cat /etc/resolv.conf')
227         child.expect('nameserver')
228         child.expect('\d+.\d+.\d+.\d+')
229         return child.after
230
231     def vm_poweroff(self, vmname, checkfail=True):
232         '''power off a VM'''
233         self.setvar('VMNAME', vmname)
234         self.run_cmd("${VM_POWEROFF}", checkfail=checkfail)
235
236     def vm_restore(self, vmname, snapshot):
237         '''restore a VM'''
238         self.setvar('VMNAME', vmname)
239         self.setvar('SNAPSHOT', snapshot)
240         self.run_cmd("${VM_RESTORE}")
241
242     def ping_wait(self, hostname):
243         '''wait for a hostname to come up on the network'''
244         hostname = self.substitute(hostname)
245         loops=10
246         while loops > 0:
247             try:
248                 self.run_cmd("ping -c 1 -w 10 %s" % hostname)
249                 break
250             except:
251                 loops = loops - 1
252         if loops == 0:
253             raise RuntimeError("Failed to ping %s" % hostname)
254         self.info("Host %s is up" % hostname)
255
256     def port_wait(self, hostname, port, retries=200, delay=3, wait_for_fail=False):
257         '''wait for a host to come up on the network'''
258         self.retry_cmd("nc -v -z -w 1 %s %u" % (hostname, port), ['succeeded'],
259                        retries=retries, delay=delay, wait_for_fail=wait_for_fail)
260
261     def run_net_time(self, child):
262         '''run net time on windows'''
263         child.sendline("net time \\\\${HOSTNAME} /set")
264         child.expect("Do you want to set the local computer")
265         child.sendline("Y")
266         child.expect("The command completed successfully")
267
268     def run_date_time(self, child, time_tuple=None):
269         '''run date and time on windows'''
270         if time_tuple is None:
271             time_tuple = time.localtime()
272         child.sendline("date")
273         child.expect("Enter the new date:")
274         i = child.expect(["dd-mm-yy", "mm-dd-yy"])
275         if i == 0:
276             child.sendline(time.strftime("%d-%m-%y", time_tuple))
277         else:
278             child.sendline(time.strftime("%m-%d-%y", time_tuple))
279         child.expect("C:")
280         child.sendline("time")
281         child.expect("Enter the new time:")
282         child.sendline(time.strftime("%H:%M:%S", time_tuple))
283         child.expect("C:")
284
285     def get_ipconfig(self, child):
286         '''get the IP configuration of the child'''
287         child.sendline("ipconfig /all")
288         child.expect('Ethernet adapter ')
289         child.expect("[\w\s]+")
290         self.setvar("WIN_NIC", child.after)
291         child.expect(['IPv4 Address', 'IP Address'])
292         child.expect('\d+.\d+.\d+.\d+')
293         self.setvar('WIN_IPV4_ADDRESS', child.after)
294         child.expect('Subnet Mask')
295         child.expect('\d+.\d+.\d+.\d+')
296         self.setvar('WIN_SUBNET_MASK', child.after)
297         child.expect('Default Gateway')
298         child.expect('\d+.\d+.\d+.\d+')
299         self.setvar('WIN_DEFAULT_GATEWAY', child.after)
300         child.expect("C:")
301
302     def run_tlntadmn(self, child):
303         '''remove the annoying telnet restrictions'''
304         child.sendline('tlntadmn config maxconn=1024')
305         child.expect("The settings were successfully updated")
306         child.expect("C:")
307
308     def disable_firewall(self, child):
309         '''remove the annoying firewall'''
310         child.sendline('netsh advfirewall set allprofiles state off')
311         i = child.expect(["Ok", "The following command was not found: advfirewall set allprofiles state off"])
312         child.expect("C:")
313         if i == 1:
314             child.sendline('netsh firewall set opmode mode = DISABLE profile = ALL')
315             child.expect("Ok")
316             child.expect("C:")
317  
318     def set_dns(self, child):
319         child.sendline('netsh interface ip set dns "${WIN_NIC}" static ${INTERFACE_IP} primary')
320         i = child.expect(['C:', pexpect.EOF, pexpect.TIMEOUT], timeout=5)
321         if i > 0:
322             return True
323         else:
324             return False
325
326     def set_ip(self, child):
327         """fix the IP address to the same value it had when we
328         connected, but don't use DHCP, and force the DNS server to our
329         DNS server.  This allows DNS updates to run"""
330         self.get_ipconfig(child)
331         child.sendline('netsh')
332         child.expect('netsh>')
333         child.sendline('offline')
334         child.expect('netsh>')
335         child.sendline('routing ip add persistentroute dest=0.0.0.0 mask=0.0.0.0 name="${WIN_NIC}" nhop=${WIN_DEFAULT_GATEWAY}')
336         child.expect('netsh>')
337         child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1 store=persistent')
338         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)
339         if i == 0:
340             child.sendline('interface ip set address "${WIN_NIC}" static ${WIN_IPV4_ADDRESS} ${WIN_SUBNET_MASK} ${WIN_DEFAULT_GATEWAY} 1')
341             child.expect('netsh>')
342         child.sendline('commit')
343         child.sendline('online')
344         child.sendline('exit')
345
346         child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=5)
347         return True
348
349
350     def resolve_ip(self, hostname, retries=60, delay=5):
351         '''resolve an IP given a hostname, assuming NBT'''
352         while retries > 0:
353             child = self.pexpect_spawn("bin/nmblookup %s" % hostname)
354             i = child.expect(['\d+.\d+.\d+.\d+', "Lookup failed"])
355             if i == 0:
356                 return child.after
357             retries -= 1
358             time.sleep(delay)
359         raise RuntimeError("Failed to resolve IP of %s" % hostname)
360
361
362     def open_telnet(self, hostname, username, password, retries=60, delay=5, set_time=False, set_ip=False,
363                     disable_firewall=True, run_tlntadmn=True):
364         '''open a telnet connection to a windows server, return the pexpect child'''
365         set_route = False
366         set_dns = False
367         if self.getvar('WIN_IP'):
368             ip = self.getvar('WIN_IP')
369         else:
370             ip = self.resolve_ip(hostname)
371             self.setvar('WIN_IP', ip)
372         while retries > 0:
373             child = self.pexpect_spawn("telnet " + ip + " -l '" + username + "'")
374             i = child.expect(["Welcome to Microsoft Telnet Service",
375                               "Denying new connections due to the limit on number of connections",
376                               "No more connections are allowed to telnet server",
377                               "Unable to connect to remote host",
378                               "No route to host",
379                               "Connection refused",
380                               pexpect.EOF])
381             if i != 0:
382                 child.close()
383                 time.sleep(delay)
384                 retries -= 1
385                 continue
386             child.expect("password:")
387             child.sendline(password)
388             i = child.expect(["C:",
389                               "Denying new connections due to the limit on number of connections",
390                               "No more connections are allowed to telnet server",
391                               "Unable to connect to remote host",
392                               "No route to host",
393                               "Connection refused",
394                               pexpect.EOF])
395             if i != 0:
396                 child.close()
397                 time.sleep(delay)
398                 retries -= 1
399                 continue
400             if set_dns:
401                 set_dns = False
402                 if self.set_dns(child):
403                     continue;
404             if set_route:
405                 child.sendline('route add 0.0.0.0 mask 0.0.0.0 ${WIN_DEFAULT_GATEWAY}')
406                 child.expect("C:")
407                 set_route = False
408             if set_time:
409                 self.run_date_time(child, None)
410                 set_time = False
411             if run_tlntadmn:
412                 self.run_tlntadmn(child)
413                 run_tlntadmn = False
414             if disable_firewall:
415                 self.disable_firewall(child)
416                 disable_firewall = False
417             if set_ip:
418                 set_ip = False
419                 if self.set_ip(child):
420                     set_route = True
421                     set_dns = True
422                 continue
423             return child
424         raise RuntimeError("Failed to connect with telnet")
425
426     def kinit(self, username, password):
427         '''use kinit to setup a credentials cache'''
428         self.run_cmd("kdestroy")
429         self.putenv('KRB5CCNAME', "${PREFIX}/ccache.test")
430         username = self.substitute(username)
431         s = username.split('@')
432         if len(s) > 0:
433             s[1] = s[1].upper()
434         username = '@'.join(s)
435         child = self.pexpect_spawn('kinit -V ' + username)
436         child.expect("Password for")
437         child.sendline(password)
438         child.expect("Authenticated to Kerberos")
439
440
441     def get_domains(self):
442         '''return a dictionary of DNS domains and IPs for named.conf'''
443         ret = {}
444         for v in self.vars:
445             if v[-6:] == "_REALM":
446                 base = v[:-6]
447                 if base + '_IP' in self.vars:
448                     ret[self.vars[base + '_REALM']] = self.vars[base + '_IP']
449         return ret