]>
Commit | Line | Data |
---|---|---|
594b1259 MW |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # topotest.py | |
5 | # Library of helper functions for NetDEF Topology Tests | |
6 | # | |
7 | # Copyright (c) 2016 by | |
8 | # Network Device Education Foundation, Inc. ("NetDEF") | |
9 | # | |
10 | # Permission to use, copy, modify, and/or distribute this software | |
11 | # for any purpose with or without fee is hereby granted, provided | |
12 | # that the above copyright notice and this permission notice appear | |
13 | # in all copies. | |
14 | # | |
15 | # THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES | |
16 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
17 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR | |
18 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY | |
19 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | |
20 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | |
21 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE | |
22 | # OF THIS SOFTWARE. | |
23 | # | |
24 | ||
7bd28cfc | 25 | import json |
594b1259 | 26 | import os |
50c40bde | 27 | import errno |
594b1259 MW |
28 | import re |
29 | import sys | |
fd858290 | 30 | import functools |
594b1259 MW |
31 | import glob |
32 | import StringIO | |
33 | import subprocess | |
1fca63c1 | 34 | import tempfile |
594b1259 | 35 | import platform |
17070436 | 36 | import difflib |
570f25d8 | 37 | import time |
594b1259 | 38 | |
6c131bd3 RZ |
39 | from lib.topolog import logger |
40 | ||
594b1259 MW |
41 | from mininet.topo import Topo |
42 | from mininet.net import Mininet | |
43 | from mininet.node import Node, OVSSwitch, Host | |
44 | from mininet.log import setLogLevel, info | |
45 | from mininet.cli import CLI | |
46 | from mininet.link import Intf | |
47 | ||
3668ed8d RZ |
48 | class json_cmp_result(object): |
49 | "json_cmp result class for better assertion messages" | |
50 | ||
51 | def __init__(self): | |
52 | self.errors = [] | |
53 | ||
54 | def add_error(self, error): | |
55 | "Append error message to the result" | |
2db5888d RZ |
56 | for line in error.splitlines(): |
57 | self.errors.append(line) | |
3668ed8d RZ |
58 | |
59 | def has_errors(self): | |
60 | "Returns True if there were errors, otherwise False." | |
61 | return len(self.errors) > 0 | |
62 | ||
7fe06d55 CF |
63 | def __str__(self): |
64 | return '\n'.join(self.errors) | |
65 | ||
da63d5b3 | 66 | |
7bd28cfc RZ |
67 | def json_diff(d1, d2): |
68 | """ | |
69 | Returns a string with the difference between JSON data. | |
70 | """ | |
71 | json_format_opts = { | |
72 | 'indent': 4, | |
73 | 'sort_keys': True, | |
74 | } | |
75 | dstr1 = json.dumps(d1, **json_format_opts) | |
76 | dstr2 = json.dumps(d2, **json_format_opts) | |
77 | return difflines(dstr2, dstr1, title1='Expected value', title2='Current value', n=0) | |
09e21b44 | 78 | |
a82e5f9a RZ |
79 | |
80 | def _json_list_cmp(list1, list2, parent, result): | |
81 | "Handles list type entries." | |
82 | # Check second list2 type | |
83 | if not isinstance(list1, type([])) or not isinstance(list2, type([])): | |
84 | result.add_error( | |
85 | '{} has different type than expected '.format(parent) + | |
86 | '(have {}, expected {}):\n{}'.format( | |
87 | type(list1), type(list2), json_diff(list1, list2))) | |
88 | return | |
89 | ||
90 | # Check list size | |
91 | if len(list2) > len(list1): | |
92 | result.add_error( | |
93 | '{} too few items '.format(parent) + | |
94 | '(have {}, expected {}:\n {})'.format( | |
95 | len(list1), len(list2), | |
96 | json_diff(list1, list2))) | |
97 | return | |
98 | ||
99 | # List all unmatched items errors | |
100 | unmatched = [] | |
101 | for expected in list2: | |
102 | matched = False | |
103 | for value in list1: | |
104 | if json_cmp({'json': value}, {'json': expected}) is None: | |
105 | matched = True | |
106 | break | |
107 | ||
a82e5f9a RZ |
108 | if not matched: |
109 | unmatched.append(expected) | |
110 | ||
111 | # If there are unmatched items, error out. | |
112 | if unmatched: | |
113 | result.add_error( | |
114 | '{} value is different (\n{})'.format( | |
115 | parent, json_diff(list1, list2))) | |
116 | ||
117 | ||
566567e9 | 118 | def json_cmp(d1, d2): |
09e21b44 RZ |
119 | """ |
120 | JSON compare function. Receives two parameters: | |
121 | * `d1`: json value | |
122 | * `d2`: json subset which we expect | |
123 | ||
124 | Returns `None` when all keys that `d1` has matches `d2`, | |
125 | otherwise a string containing what failed. | |
126 | ||
127 | Note: key absence can be tested by adding a key with value `None`. | |
128 | """ | |
3668ed8d RZ |
129 | squeue = [(d1, d2, 'json')] |
130 | result = json_cmp_result() | |
a82e5f9a | 131 | |
09e21b44 | 132 | for s in squeue: |
3668ed8d | 133 | nd1, nd2, parent = s |
a82e5f9a RZ |
134 | |
135 | # Handle JSON beginning with lists. | |
136 | if isinstance(nd1, type([])) or isinstance(nd2, type([])): | |
137 | _json_list_cmp(nd1, nd2, parent, result) | |
138 | if result.has_errors(): | |
139 | return result | |
140 | else: | |
141 | return None | |
09e21b44 RZ |
142 | |
143 | # Expect all required fields to exist. | |
a82e5f9a | 144 | s1, s2 = set(nd1), set(nd2) |
09e21b44 RZ |
145 | s2_req = set([key for key in nd2 if nd2[key] is not None]) |
146 | diff = s2_req - s1 | |
147 | if diff != set({}): | |
08533b7b RZ |
148 | result.add_error('expected key(s) {} in {} (have {}):\n{}'.format( |
149 | str(list(diff)), parent, str(list(s1)), json_diff(nd1, nd2))) | |
09e21b44 RZ |
150 | |
151 | for key in s2.intersection(s1): | |
152 | # Test for non existence of key in d2 | |
153 | if nd2[key] is None: | |
08533b7b RZ |
154 | result.add_error('"{}" should not exist in {} (have {}):\n{}'.format( |
155 | key, parent, str(s1), json_diff(nd1[key], nd2[key]))) | |
3668ed8d | 156 | continue |
a82e5f9a | 157 | |
09e21b44 RZ |
158 | # If nd1 key is a dict, we have to recurse in it later. |
159 | if isinstance(nd2[key], type({})): | |
dc0d3fc5 RZ |
160 | if not isinstance(nd1[key], type({})): |
161 | result.add_error( | |
162 | '{}["{}"] has different type than expected '.format(parent, key) + | |
08533b7b RZ |
163 | '(have {}, expected {}):\n{}'.format( |
164 | type(nd1[key]), type(nd2[key]), json_diff(nd1[key], nd2[key]))) | |
dc0d3fc5 | 165 | continue |
3668ed8d RZ |
166 | nparent = '{}["{}"]'.format(parent, key) |
167 | squeue.append((nd1[key], nd2[key], nparent)) | |
09e21b44 | 168 | continue |
a82e5f9a | 169 | |
dc0d3fc5 RZ |
170 | # Check list items |
171 | if isinstance(nd2[key], type([])): | |
a82e5f9a | 172 | _json_list_cmp(nd1[key], nd2[key], parent, result) |
dc0d3fc5 RZ |
173 | continue |
174 | ||
09e21b44 RZ |
175 | # Compare JSON values |
176 | if nd1[key] != nd2[key]: | |
3668ed8d | 177 | result.add_error( |
7bd28cfc RZ |
178 | '{}["{}"] value is different (\n{})'.format( |
179 | parent, key, json_diff(nd1[key], nd2[key]))) | |
3668ed8d RZ |
180 | continue |
181 | ||
182 | if result.has_errors(): | |
183 | return result | |
09e21b44 RZ |
184 | |
185 | return None | |
186 | ||
a82e5f9a | 187 | |
5cffda18 RZ |
188 | def router_output_cmp(router, cmd, expected): |
189 | """ | |
190 | Runs `cmd` in router and compares the output with `expected`. | |
191 | """ | |
192 | return difflines(normalize_text(router.vtysh_cmd(cmd)), | |
193 | normalize_text(expected), | |
194 | title1="Current output", | |
195 | title2="Expected output") | |
196 | ||
197 | ||
198 | def router_json_cmp(router, cmd, data): | |
199 | """ | |
200 | Runs `cmd` that returns JSON data (normally the command ends with 'json') | |
201 | and compare with `data` contents. | |
202 | """ | |
203 | return json_cmp(router.vtysh_cmd(cmd, isjson=True), data) | |
204 | ||
205 | ||
1fca63c1 RZ |
206 | def run_and_expect(func, what, count=20, wait=3): |
207 | """ | |
208 | Run `func` and compare the result with `what`. Do it for `count` times | |
209 | waiting `wait` seconds between tries. By default it tries 20 times with | |
210 | 3 seconds delay between tries. | |
211 | ||
212 | Returns (True, func-return) on success or | |
213 | (False, func-return) on failure. | |
5cffda18 RZ |
214 | |
215 | --- | |
216 | ||
217 | Helper functions to use with this function: | |
218 | - router_output_cmp | |
219 | - router_json_cmp | |
1fca63c1 | 220 | """ |
fd858290 RZ |
221 | start_time = time.time() |
222 | func_name = "<unknown>" | |
223 | if func.__class__ == functools.partial: | |
224 | func_name = func.func.__name__ | |
225 | else: | |
226 | func_name = func.__name__ | |
227 | ||
228 | logger.info( | |
229 | "'{}' polling started (interval {} secs, maximum wait {} secs)".format( | |
230 | func_name, wait, int(wait * count))) | |
231 | ||
1fca63c1 RZ |
232 | while count > 0: |
233 | result = func() | |
234 | if result != what: | |
570f25d8 | 235 | time.sleep(wait) |
1fca63c1 RZ |
236 | count -= 1 |
237 | continue | |
fd858290 RZ |
238 | |
239 | end_time = time.time() | |
240 | logger.info("'{}' succeeded after {:.2f} seconds".format( | |
241 | func_name, end_time - start_time)) | |
1fca63c1 | 242 | return (True, result) |
fd858290 RZ |
243 | |
244 | end_time = time.time() | |
245 | logger.error("'{}' failed after {:.2f} seconds".format( | |
246 | func_name, end_time - start_time)) | |
1fca63c1 RZ |
247 | return (False, result) |
248 | ||
249 | ||
594b1259 MW |
250 | def int2dpid(dpid): |
251 | "Converting Integer to DPID" | |
252 | ||
253 | try: | |
254 | dpid = hex(dpid)[2:] | |
255 | dpid = '0'*(16-len(dpid))+dpid | |
256 | return dpid | |
257 | except IndexError: | |
258 | raise Exception('Unable to derive default datapath ID - ' | |
259 | 'please either specify a dpid or use a ' | |
260 | 'canonical switch name such as s23.') | |
261 | ||
50c40bde MW |
262 | def pid_exists(pid): |
263 | "Check whether pid exists in the current process table." | |
264 | ||
265 | if pid <= 0: | |
266 | return False | |
267 | try: | |
268 | os.kill(pid, 0) | |
269 | except OSError as err: | |
270 | if err.errno == errno.ESRCH: | |
271 | # ESRCH == No such process | |
272 | return False | |
273 | elif err.errno == errno.EPERM: | |
274 | # EPERM clearly means there's a process to deny access to | |
275 | return True | |
276 | else: | |
277 | # According to "man 2 kill" possible error values are | |
278 | # (EINVAL, EPERM, ESRCH) | |
279 | raise | |
280 | else: | |
281 | return True | |
282 | ||
bc2872fd | 283 | def get_textdiff(text1, text2, title1="", title2="", **opts): |
17070436 MW |
284 | "Returns empty string if same or formatted diff" |
285 | ||
91733ef8 | 286 | diff = '\n'.join(difflib.unified_diff(text1, text2, |
bc2872fd | 287 | fromfile=title1, tofile=title2, **opts)) |
17070436 MW |
288 | # Clean up line endings |
289 | diff = os.linesep.join([s for s in diff.splitlines() if s]) | |
290 | return diff | |
291 | ||
bc2872fd | 292 | def difflines(text1, text2, title1='', title2='', **opts): |
1fca63c1 RZ |
293 | "Wrapper for get_textdiff to avoid string transformations." |
294 | text1 = ('\n'.join(text1.rstrip().splitlines()) + '\n').splitlines(1) | |
295 | text2 = ('\n'.join(text2.rstrip().splitlines()) + '\n').splitlines(1) | |
bc2872fd | 296 | return get_textdiff(text1, text2, title1, title2, **opts) |
1fca63c1 RZ |
297 | |
298 | def get_file(content): | |
299 | """ | |
300 | Generates a temporary file in '/tmp' with `content` and returns the file name. | |
301 | """ | |
302 | fde = tempfile.NamedTemporaryFile(mode='w', delete=False) | |
303 | fname = fde.name | |
304 | fde.write(content) | |
305 | fde.close() | |
306 | return fname | |
307 | ||
f7840f6b RZ |
308 | def normalize_text(text): |
309 | """ | |
9683a1bb | 310 | Strips formating spaces/tabs, carriage returns and trailing whitespace. |
f7840f6b RZ |
311 | """ |
312 | text = re.sub(r'[ \t]+', ' ', text) | |
313 | text = re.sub(r'\r', '', text) | |
9683a1bb RZ |
314 | |
315 | # Remove whitespace in the middle of text. | |
316 | text = re.sub(r'[ \t]+\n', '\n', text) | |
317 | # Remove whitespace at the end of the text. | |
318 | text = text.rstrip() | |
319 | ||
f7840f6b RZ |
320 | return text |
321 | ||
cc95fbd9 | 322 | def module_present_linux(module, load): |
f2d6ce41 CF |
323 | """ |
324 | Returns whether `module` is present. | |
325 | ||
326 | If `load` is true, it will try to load it via modprobe. | |
327 | """ | |
328 | with open('/proc/modules', 'r') as modules_file: | |
329 | if module.replace('-','_') in modules_file.read(): | |
330 | return True | |
331 | cmd = '/sbin/modprobe {}{}'.format('' if load else '-n ', | |
332 | module) | |
333 | if os.system(cmd) != 0: | |
334 | return False | |
335 | else: | |
336 | return True | |
337 | ||
cc95fbd9 DS |
338 | def module_present_freebsd(module, load): |
339 | return True | |
340 | ||
341 | def module_present(module, load=True): | |
342 | if sys.platform.startswith("linux"): | |
28440fd9 | 343 | return module_present_linux(module, load) |
cc95fbd9 | 344 | elif sys.platform.startswith("freebsd"): |
28440fd9 | 345 | return module_present_freebsd(module, load) |
cc95fbd9 | 346 | |
4190fe1e RZ |
347 | def version_cmp(v1, v2): |
348 | """ | |
349 | Compare two version strings and returns: | |
350 | ||
351 | * `-1`: if `v1` is less than `v2` | |
352 | * `0`: if `v1` is equal to `v2` | |
353 | * `1`: if `v1` is greater than `v2` | |
354 | ||
355 | Raises `ValueError` if versions are not well formated. | |
356 | """ | |
357 | vregex = r'(?P<whole>\d+(\.(\d+))*)' | |
358 | v1m = re.match(vregex, v1) | |
359 | v2m = re.match(vregex, v2) | |
360 | if v1m is None or v2m is None: | |
361 | raise ValueError("got a invalid version string") | |
362 | ||
363 | # Split values | |
364 | v1g = v1m.group('whole').split('.') | |
365 | v2g = v2m.group('whole').split('.') | |
366 | ||
367 | # Get the longest version string | |
368 | vnum = len(v1g) | |
369 | if len(v2g) > vnum: | |
370 | vnum = len(v2g) | |
371 | ||
372 | # Reverse list because we are going to pop the tail | |
373 | v1g.reverse() | |
374 | v2g.reverse() | |
375 | for _ in range(vnum): | |
376 | try: | |
377 | v1n = int(v1g.pop()) | |
378 | except IndexError: | |
379 | while v2g: | |
380 | v2n = int(v2g.pop()) | |
381 | if v2n > 0: | |
382 | return -1 | |
383 | break | |
384 | ||
385 | try: | |
386 | v2n = int(v2g.pop()) | |
387 | except IndexError: | |
388 | if v1n > 0: | |
389 | return 1 | |
390 | while v1g: | |
391 | v1n = int(v1g.pop()) | |
392 | if v1n > 0: | |
034237db | 393 | return 1 |
4190fe1e RZ |
394 | break |
395 | ||
396 | if v1n > v2n: | |
397 | return 1 | |
398 | if v1n < v2n: | |
399 | return -1 | |
400 | return 0 | |
401 | ||
f5612168 PG |
402 | def interface_set_status(node, ifacename, ifaceaction=False, vrf_name=None): |
403 | if ifaceaction: | |
404 | str_ifaceaction = 'no shutdown' | |
405 | else: | |
406 | str_ifaceaction = 'shutdown' | |
407 | if vrf_name == None: | |
408 | cmd = 'vtysh -c \"configure terminal\" -c \"interface {0}\" -c \"{1}\"'.format(ifacename, str_ifaceaction) | |
409 | else: | |
410 | cmd = 'vtysh -c \"configure terminal\" -c \"interface {0} vrf {1}\" -c \"{2}\"'.format(ifacename, vrf_name, str_ifaceaction) | |
411 | node.run(cmd) | |
412 | ||
b220b3c8 PG |
413 | def ip4_route_zebra(node, vrf_name=None): |
414 | """ | |
415 | Gets an output of 'show ip route' command. It can be used | |
416 | with comparing the output to a reference | |
417 | """ | |
418 | if vrf_name == None: | |
419 | tmp = node.vtysh_cmd('show ip route') | |
420 | else: | |
421 | tmp = node.vtysh_cmd('show ip route vrf {0}'.format(vrf_name)) | |
422 | output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp) | |
41077aa1 CF |
423 | |
424 | lines = output.splitlines() | |
425 | header_found = False | |
0eff5820 | 426 | while lines and (not lines[0].strip() or not header_found): |
41077aa1 CF |
427 | if '> - selected route' in lines[0]: |
428 | header_found = True | |
429 | lines = lines[1:] | |
430 | return '\n'.join(lines) | |
b220b3c8 | 431 | |
e394d9aa MS |
432 | def ip6_route_zebra(node, vrf_name=None): |
433 | """ | |
434 | Retrieves the output of 'show ipv6 route [vrf vrf_name]', then | |
435 | canonicalizes it by eliding link-locals. | |
436 | """ | |
437 | ||
438 | if vrf_name == None: | |
439 | tmp = node.vtysh_cmd('show ipv6 route') | |
440 | else: | |
441 | tmp = node.vtysh_cmd('show ipv6 route vrf {0}'.format(vrf_name)) | |
442 | ||
443 | # Mask out timestamp | |
444 | output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp) | |
445 | ||
446 | # Mask out the link-local addresses | |
447 | output = re.sub(r'fe80::[^ ]+,', 'fe80::XXXX:XXXX:XXXX:XXXX,', output) | |
448 | ||
449 | lines = output.splitlines() | |
450 | header_found = False | |
451 | while lines and (not lines[0].strip() or not header_found): | |
452 | if '> - selected route' in lines[0]: | |
453 | header_found = True | |
454 | lines = lines[1:] | |
455 | ||
456 | return '\n'.join(lines) | |
457 | ||
458 | ||
2f726781 MW |
459 | def proto_name_to_number(protocol): |
460 | return { | |
461 | 'bgp': '186', | |
462 | 'isis': '187', | |
463 | 'ospf': '188', | |
464 | 'rip': '189', | |
465 | 'ripng': '190', | |
466 | 'nhrp': '191', | |
467 | 'eigrp': '192', | |
468 | 'ldp': '193', | |
469 | 'sharp': '194', | |
470 | 'pbr': '195', | |
471 | 'static': '196' | |
472 | }.get(protocol, protocol) # default return same as input | |
473 | ||
474 | ||
99a7a912 RZ |
475 | def ip4_route(node): |
476 | """ | |
477 | Gets a structured return of the command 'ip route'. It can be used in | |
478 | conjuction with json_cmp() to provide accurate assert explanations. | |
479 | ||
480 | Return example: | |
481 | { | |
482 | '10.0.1.0/24': { | |
483 | 'dev': 'eth0', | |
484 | 'via': '172.16.0.1', | |
485 | 'proto': '188', | |
486 | }, | |
487 | '10.0.2.0/24': { | |
488 | 'dev': 'eth1', | |
489 | 'proto': 'kernel', | |
490 | } | |
491 | } | |
492 | """ | |
493 | output = normalize_text(node.run('ip route')).splitlines() | |
494 | result = {} | |
495 | for line in output: | |
496 | columns = line.split(' ') | |
497 | route = result[columns[0]] = {} | |
498 | prev = None | |
499 | for column in columns: | |
500 | if prev == 'dev': | |
501 | route['dev'] = column | |
502 | if prev == 'via': | |
503 | route['via'] = column | |
504 | if prev == 'proto': | |
2f726781 MW |
505 | # translate protocol names back to numbers |
506 | route['proto'] = proto_name_to_number(column) | |
99a7a912 RZ |
507 | if prev == 'metric': |
508 | route['metric'] = column | |
509 | if prev == 'scope': | |
510 | route['scope'] = column | |
511 | prev = column | |
512 | ||
513 | return result | |
514 | ||
515 | def ip6_route(node): | |
516 | """ | |
517 | Gets a structured return of the command 'ip -6 route'. It can be used in | |
518 | conjuction with json_cmp() to provide accurate assert explanations. | |
519 | ||
520 | Return example: | |
521 | { | |
522 | '2001:db8:1::/64': { | |
523 | 'dev': 'eth0', | |
524 | 'proto': '188', | |
525 | }, | |
526 | '2001:db8:2::/64': { | |
527 | 'dev': 'eth1', | |
528 | 'proto': 'kernel', | |
529 | } | |
530 | } | |
531 | """ | |
532 | output = normalize_text(node.run('ip -6 route')).splitlines() | |
533 | result = {} | |
534 | for line in output: | |
535 | columns = line.split(' ') | |
536 | route = result[columns[0]] = {} | |
537 | prev = None | |
538 | for column in columns: | |
539 | if prev == 'dev': | |
540 | route['dev'] = column | |
541 | if prev == 'via': | |
542 | route['via'] = column | |
543 | if prev == 'proto': | |
2f726781 MW |
544 | # translate protocol names back to numbers |
545 | route['proto'] = proto_name_to_number(column) | |
99a7a912 RZ |
546 | if prev == 'metric': |
547 | route['metric'] = column | |
548 | if prev == 'pref': | |
549 | route['pref'] = column | |
550 | prev = column | |
551 | ||
552 | return result | |
553 | ||
570f25d8 RZ |
554 | def sleep(amount, reason=None): |
555 | """ | |
556 | Sleep wrapper that registers in the log the amount of sleep | |
557 | """ | |
558 | if reason is None: | |
559 | logger.info('Sleeping for {} seconds'.format(amount)) | |
560 | else: | |
561 | logger.info(reason + ' ({} seconds)'.format(amount)) | |
562 | ||
563 | time.sleep(amount) | |
564 | ||
4942f298 MW |
565 | def checkAddressSanitizerError(output, router, component): |
566 | "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise" | |
567 | ||
568 | addressSantizerError = re.search('(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ', output) | |
569 | if addressSantizerError: | |
570 | sys.stderr.write("%s: %s triggered an exception by AddressSanitizer\n" % (router, component)) | |
571 | # Sanitizer Error found in log | |
572 | pidMark = addressSantizerError.group(1) | |
573 | addressSantizerLog = re.search('%s(.*)%s' % (pidMark, pidMark), output, re.DOTALL) | |
574 | if addressSantizerLog: | |
575 | callingTest = os.path.basename(sys._current_frames().values()[0].f_back.f_back.f_globals['__file__']) | |
576 | callingProc = sys._getframe(2).f_code.co_name | |
577 | with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile: | |
578 | sys.stderr.write('\n'.join(addressSantizerLog.group(1).splitlines()) + '\n') | |
579 | addrSanFile.write("## Error: %s\n\n" % addressSantizerError.group(2)) | |
580 | addrSanFile.write("### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" % (callingTest, callingProc, router)) | |
581 | addrSanFile.write(' '+ '\n '.join(addressSantizerLog.group(1).splitlines()) + '\n') | |
582 | addrSanFile.write("\n---------------\n") | |
583 | return True | |
6c131bd3 | 584 | return False |
4942f298 | 585 | |
594b1259 | 586 | def addRouter(topo, name): |
80eeefb7 | 587 | "Adding a FRRouter (or Quagga) to Topology" |
594b1259 MW |
588 | |
589 | MyPrivateDirs = ['/etc/frr', | |
590 | '/etc/quagga', | |
591 | '/var/run/frr', | |
592 | '/var/run/quagga', | |
593 | '/var/log'] | |
4b1d6d4d DS |
594 | if sys.platform.startswith("linux"): |
595 | return topo.addNode(name, cls=LinuxRouter, privateDirs=MyPrivateDirs) | |
596 | elif sys.platform.startswith("freebsd"): | |
597 | return topo.addNode(name, cls=FreeBSDRouter, privateDirs=MyPrivateDirs) | |
594b1259 | 598 | |
797e8dcf RZ |
599 | def set_sysctl(node, sysctl, value): |
600 | "Set a sysctl value and return None on success or an error string" | |
601 | valuestr = '{}'.format(value) | |
602 | command = "sysctl {0}={1}".format(sysctl, valuestr) | |
603 | cmdret = node.cmd(command) | |
604 | ||
605 | matches = re.search(r'([^ ]+) = ([^\s]+)', cmdret) | |
606 | if matches is None: | |
607 | return cmdret | |
608 | if matches.group(1) != sysctl: | |
609 | return cmdret | |
610 | if matches.group(2) != valuestr: | |
611 | return cmdret | |
612 | ||
613 | return None | |
614 | ||
615 | def assert_sysctl(node, sysctl, value): | |
616 | "Set and assert that the sysctl is set with the specified value." | |
617 | assert set_sysctl(node, sysctl, value) is None | |
618 | ||
594b1259 MW |
619 | |
620 | class Router(Node): | |
621 | "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine" | |
622 | ||
2ab85530 RZ |
623 | def __init__(self, name, **params): |
624 | super(Router, self).__init__(name, **params) | |
0d5e41c6 RZ |
625 | self.logdir = params.get('logdir') |
626 | ||
627 | # If this topology is using old API and doesn't have logdir | |
628 | # specified, then attempt to generate an unique logdir. | |
629 | if self.logdir is None: | |
630 | cur_test = os.environ['PYTEST_CURRENT_TEST'] | |
631 | self.logdir = ('/tmp/topotests/' + | |
632 | cur_test[0:cur_test.find(".py")].replace('/', '.')) | |
633 | ||
634 | # If the logdir is not created, then create it and set the | |
635 | # appropriated permissions. | |
636 | if not os.path.isdir(self.logdir): | |
637 | os.system('mkdir -p ' + self.logdir + '/' + name) | |
638 | os.system('chmod -R go+rw /tmp/topotests') | |
639 | ||
2ab85530 | 640 | self.daemondir = None |
447f2d5a | 641 | self.hasmpls = False |
2ab85530 RZ |
642 | self.routertype = 'frr' |
643 | self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0, | |
644 | 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0, | |
4d45d6d3 | 645 | 'ldpd': 0, 'eigrpd': 0, 'nhrpd': 0, 'staticd': 0, |
849d1ed9 | 646 | 'bfdd': 0, 'sharpd': 0} |
8dd5077d | 647 | self.daemons_options = {'zebra': ''} |
2a59a86b | 648 | self.reportCores = True |
fb80b81b | 649 | self.version = None |
2ab85530 | 650 | |
edd2bdf6 RZ |
651 | def _config_frr(self, **params): |
652 | "Configure FRR binaries" | |
653 | self.daemondir = params.get('frrdir') | |
654 | if self.daemondir is None: | |
655 | self.daemondir = '/usr/lib/frr' | |
656 | ||
657 | zebra_path = os.path.join(self.daemondir, 'zebra') | |
658 | if not os.path.isfile(zebra_path): | |
659 | raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path)) | |
660 | ||
661 | def _config_quagga(self, **params): | |
662 | "Configure Quagga binaries" | |
663 | self.daemondir = params.get('quaggadir') | |
664 | if self.daemondir is None: | |
665 | self.daemondir = '/usr/lib/quagga' | |
666 | ||
667 | zebra_path = os.path.join(self.daemondir, 'zebra') | |
668 | if not os.path.isfile(zebra_path): | |
669 | raise Exception("Quagga zebra binary doesn't exist at {}".format(zebra_path)) | |
670 | ||
2ab85530 RZ |
671 | # pylint: disable=W0221 |
672 | # Some params are only meaningful for the parent class. | |
594b1259 MW |
673 | def config(self, **params): |
674 | super(Router, self).config(**params) | |
675 | ||
2ab85530 RZ |
676 | # User did not specify the daemons directory, try to autodetect it. |
677 | self.daemondir = params.get('daemondir') | |
678 | if self.daemondir is None: | |
edd2bdf6 RZ |
679 | self.routertype = params.get('routertype', 'frr') |
680 | if self.routertype == 'quagga': | |
681 | self._config_quagga(**params) | |
682 | else: | |
683 | self._config_frr(**params) | |
594b1259 | 684 | else: |
2ab85530 RZ |
685 | # Test the provided path |
686 | zpath = os.path.join(self.daemondir, 'zebra') | |
687 | if not os.path.isfile(zpath): | |
688 | raise Exception('No zebra binary found in {}'.format(zpath)) | |
689 | # Allow user to specify routertype when the path was specified. | |
690 | if params.get('routertype') is not None: | |
691 | self.routertype = self.params.get('routertype') | |
692 | ||
594b1259 MW |
693 | self.cmd('ulimit -c unlimited') |
694 | # Set ownership of config files | |
2ab85530 RZ |
695 | self.cmd('chown {0}:{0}vty /etc/{0}'.format(self.routertype)) |
696 | ||
594b1259 MW |
697 | def terminate(self): |
698 | # Delete Running Quagga or FRR Daemons | |
99561211 MW |
699 | self.stopRouter() |
700 | # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype) | |
701 | # for d in StringIO.StringIO(rundaemons): | |
702 | # self.cmd('kill -7 `cat %s`' % d.rstrip()) | |
703 | # self.waitOutput() | |
594b1259 | 704 | # Disable forwarding |
797e8dcf RZ |
705 | set_sysctl(self, 'net.ipv4.ip_forward', 0) |
706 | set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0) | |
594b1259 | 707 | super(Router, self).terminate() |
b0f0d980 LB |
708 | os.system('chmod -R go+rw /tmp/topotests') |
709 | ||
dce382d4 | 710 | def stopRouter(self, wait=True, assertOnError=True, minErrorVersion='5.1'): |
99561211 MW |
711 | # Stop Running Quagga or FRR Daemons |
712 | rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype) | |
83c26937 | 713 | errors = "" |
e600b2d9 | 714 | if re.search(r"No such file or directory", rundaemons): |
83c26937 | 715 | return errors |
99561211 | 716 | if rundaemons is not None: |
3a568b9c | 717 | numRunning = 0 |
7551168c MW |
718 | for d in StringIO.StringIO(rundaemons): |
719 | daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip() | |
720 | if (daemonpid.isdigit() and pid_exists(int(daemonpid))): | |
065bd557 RZ |
721 | logger.info('{}: stopping {}'.format( |
722 | self.name, | |
723 | os.path.basename(d.rstrip().rsplit(".", 1)[0]) | |
724 | )) | |
7551168c MW |
725 | self.cmd('kill -TERM %s' % daemonpid) |
726 | self.waitOutput() | |
3a568b9c LB |
727 | if pid_exists(int(daemonpid)): |
728 | numRunning += 1 | |
729 | if wait and numRunning > 0: | |
065bd557 | 730 | sleep(2, '{}: waiting for daemons stopping'.format(self.name)) |
3a568b9c LB |
731 | # 2nd round of kill if daemons didn't exit |
732 | for d in StringIO.StringIO(rundaemons): | |
733 | daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip() | |
734 | if (daemonpid.isdigit() and pid_exists(int(daemonpid))): | |
065bd557 RZ |
735 | logger.info('{}: killing {}'.format( |
736 | self.name, | |
737 | os.path.basename(d.rstrip().rsplit(".", 1)[0]) | |
738 | )) | |
3a568b9c LB |
739 | self.cmd('kill -7 %s' % daemonpid) |
740 | self.waitOutput() | |
e600b2d9 | 741 | self.cmd('rm -- {}'.format(d.rstrip())) |
f76774ec | 742 | if wait: |
83c26937 | 743 | errors = self.checkRouterCores(reportOnce=True) |
436aa319 LB |
744 | if self.checkRouterVersion('<', minErrorVersion): |
745 | #ignore errors in old versions | |
746 | errors = "" | |
83c26937 LB |
747 | if assertOnError and len(errors) > 0: |
748 | assert "Errors found - details follow:" == 0, errors | |
749 | return errors | |
f76774ec | 750 | |
594b1259 MW |
751 | def removeIPs(self): |
752 | for interface in self.intfNames(): | |
753 | self.cmd('ip address flush', interface) | |
8dd5077d PG |
754 | |
755 | def checkCapability(self, daemon, param): | |
756 | if param is not None: | |
757 | daemon_path = os.path.join(self.daemondir, daemon) | |
758 | daemon_search_option = param.replace('-','') | |
759 | output = self.cmd('{0} -h | grep {1}'.format( | |
760 | daemon_path, daemon_search_option)) | |
761 | if daemon_search_option not in output: | |
762 | return False | |
763 | return True | |
764 | ||
765 | def loadConf(self, daemon, source=None, param=None): | |
594b1259 MW |
766 | # print "Daemons before:", self.daemons |
767 | if daemon in self.daemons.keys(): | |
768 | self.daemons[daemon] = 1 | |
8dd5077d PG |
769 | if param is not None: |
770 | self.daemons_options[daemon] = param | |
594b1259 MW |
771 | if source is None: |
772 | self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon)) | |
773 | self.waitOutput() | |
774 | else: | |
775 | self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon)) | |
776 | self.waitOutput() | |
777 | self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon)) | |
778 | self.waitOutput() | |
779 | self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon)) | |
780 | self.waitOutput() | |
2c805e6c | 781 | if (daemon == 'zebra') and (self.daemons['staticd'] == 0): |
a2a1134c MW |
782 | # Add staticd with zebra - if it exists |
783 | staticd_path = os.path.join(self.daemondir, 'staticd') | |
784 | if os.path.isfile(staticd_path): | |
785 | self.daemons['staticd'] = 1 | |
2c805e6c MW |
786 | self.daemons_options['staticd'] = '' |
787 | # Auto-Started staticd has no config, so it will read from zebra config | |
594b1259 | 788 | else: |
222ea88b | 789 | logger.info('No daemon {} known'.format(daemon)) |
594b1259 | 790 | # print "Daemons after:", self.daemons |
e1dfa45e | 791 | |
9711fc7e | 792 | def startRouter(self, tgen=None): |
594b1259 | 793 | # Disable integrated-vtysh-config |
a93477ec | 794 | self.cmd('echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' % self.routertype) |
594b1259 | 795 | self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype)) |
13e1fc49 | 796 | # TODO remove the following lines after all tests are migrated to Topogen. |
594b1259 | 797 | # Try to find relevant old logfiles in /tmp and delete them |
e1dfa45e | 798 | map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name))) |
594b1259 | 799 | # Remove old core files |
e1dfa45e | 800 | map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name))) |
594b1259 MW |
801 | # Remove IP addresses from OS first - we have them in zebra.conf |
802 | self.removeIPs() | |
803 | # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher | |
804 | # No error - but return message and skip all the tests | |
805 | if self.daemons['ldpd'] == 1: | |
2ab85530 RZ |
806 | ldpd_path = os.path.join(self.daemondir, 'ldpd') |
807 | if not os.path.isfile(ldpd_path): | |
222ea88b | 808 | logger.info("LDP Test, but no ldpd compiled or installed") |
594b1259 | 809 | return "LDP Test, but no ldpd compiled or installed" |
dd4eca4d | 810 | |
45619ee3 | 811 | if version_cmp(platform.release(), '4.5') < 0: |
222ea88b | 812 | logger.info("LDP Test need Linux Kernel 4.5 minimum") |
45619ee3 | 813 | return "LDP Test need Linux Kernel 4.5 minimum" |
9711fc7e LB |
814 | # Check if have mpls |
815 | if tgen != None: | |
816 | self.hasmpls = tgen.hasmpls | |
817 | if self.hasmpls != True: | |
818 | logger.info("LDP/MPLS Tests will be skipped, platform missing module(s)") | |
819 | else: | |
820 | # Test for MPLS Kernel modules available | |
821 | self.hasmpls = False | |
f2d6ce41 | 822 | if not module_present('mpls-router'): |
9711fc7e | 823 | logger.info('MPLS tests will not run (missing mpls-router kernel module)') |
f2d6ce41 | 824 | elif not module_present('mpls-iptunnel'): |
9711fc7e LB |
825 | logger.info('MPLS tests will not run (missing mpls-iptunnel kernel module)') |
826 | else: | |
827 | self.hasmpls = True | |
828 | if self.hasmpls != True: | |
829 | return "LDP/MPLS Tests need mpls kernel modules" | |
a93eb16a | 830 | self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels') |
44a592b2 MW |
831 | |
832 | if self.daemons['eigrpd'] == 1: | |
833 | eigrpd_path = os.path.join(self.daemondir, 'eigrpd') | |
834 | if not os.path.isfile(eigrpd_path): | |
222ea88b | 835 | logger.info("EIGRP Test, but no eigrpd compiled or installed") |
44a592b2 MW |
836 | return "EIGRP Test, but no eigrpd compiled or installed" |
837 | ||
4d45d6d3 RZ |
838 | if self.daemons['bfdd'] == 1: |
839 | bfdd_path = os.path.join(self.daemondir, 'bfdd') | |
840 | if not os.path.isfile(bfdd_path): | |
841 | logger.info("BFD Test, but no bfdd compiled or installed") | |
842 | return "BFD Test, but no bfdd compiled or installed" | |
843 | ||
99561211 MW |
844 | self.restartRouter() |
845 | return "" | |
e1dfa45e | 846 | |
99561211 | 847 | def restartRouter(self): |
e1dfa45e LB |
848 | # Starts actual daemons without init (ie restart) |
849 | # cd to per node directory | |
850 | self.cmd('cd {}/{}'.format(self.logdir, self.name)) | |
b0f0d980 | 851 | self.cmd('umask 000') |
2a59a86b LB |
852 | #Re-enable to allow for report per run |
853 | self.reportCores = True | |
fb80b81b LB |
854 | if self.version == None: |
855 | self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2] | |
856 | logger.info('{}: running version: {}'.format(self.name,self.version)) | |
594b1259 MW |
857 | # Start Zebra first |
858 | if self.daemons['zebra'] == 1: | |
2ab85530 | 859 | zebra_path = os.path.join(self.daemondir, 'zebra') |
8dd5077d | 860 | zebra_option = self.daemons_options['zebra'] |
e1dfa45e | 861 | self.cmd('{0} {1} > zebra.out 2> zebra.err &'.format( |
8dd5077d | 862 | zebra_path, zebra_option, self.logdir, self.name |
2ab85530 | 863 | )) |
594b1259 | 864 | self.waitOutput() |
6c131bd3 | 865 | logger.debug('{}: {} zebra started'.format(self, self.routertype)) |
63038f4b | 866 | sleep(1, '{}: waiting for zebra to start'.format(self.name)) |
a2a1134c MW |
867 | # Start staticd next if required |
868 | if self.daemons['staticd'] == 1: | |
869 | staticd_path = os.path.join(self.daemondir, 'staticd') | |
870 | staticd_option = self.daemons_options['staticd'] | |
871 | self.cmd('{0} {1} > staticd.out 2> staticd.err &'.format( | |
872 | staticd_path, staticd_option, self.logdir, self.name | |
873 | )) | |
874 | self.waitOutput() | |
875 | logger.debug('{}: {} staticd started'.format(self, self.routertype)) | |
a2a1134c | 876 | # Fix Link-Local Addresses |
594b1259 MW |
877 | # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this |
878 | self.cmd('for i in `ls /sys/class/net/` ; do mac=`cat /sys/class/net/$i/address`; IFS=\':\'; set $mac; unset IFS; ip address add dev $i scope link fe80::$(printf %02x $((0x$1 ^ 2)))$2:${3}ff:fe$4:$5$6/64; done') | |
879 | # Now start all the other daemons | |
880 | for daemon in self.daemons: | |
2ab85530 | 881 | # Skip disabled daemons and zebra |
a2a1134c | 882 | if self.daemons[daemon] == 0 or daemon == 'zebra' or daemon == 'staticd': |
2ab85530 | 883 | continue |
2ab85530 | 884 | daemon_path = os.path.join(self.daemondir, daemon) |
86c21ac7 | 885 | self.cmd('{0} {1} > {2}.out 2> {2}.err &'.format( |
886 | daemon_path, self.daemons_options.get(daemon, ''), daemon | |
887 | )) | |
2ab85530 | 888 | self.waitOutput() |
6c131bd3 | 889 | logger.debug('{}: {} {} started'.format(self, self.routertype, daemon)) |
99561211 MW |
890 | def getStdErr(self, daemon): |
891 | return self.getLog('err', daemon) | |
892 | def getStdOut(self, daemon): | |
893 | return self.getLog('out', daemon) | |
894 | def getLog(self, log, daemon): | |
e1dfa45e | 895 | return self.cmd('cat {}/{}/{}.{}'.format(self.logdir, self.name, daemon, log)) |
f76774ec | 896 | |
2a59a86b LB |
897 | def checkRouterCores(self, reportLeaks=True, reportOnce=False): |
898 | if reportOnce and not self.reportCores: | |
899 | return | |
900 | reportMade = False | |
83c26937 | 901 | traces = "" |
f76774ec LB |
902 | for daemon in self.daemons: |
903 | if (self.daemons[daemon] == 1): | |
904 | # Look for core file | |
905 | corefiles = glob.glob('{}/{}/{}_core*.dmp'.format( | |
906 | self.logdir, self.name, daemon)) | |
907 | if (len(corefiles) > 0): | |
908 | daemon_path = os.path.join(self.daemondir, daemon) | |
909 | backtrace = subprocess.check_output([ | |
910 | "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0]) | |
911 | ], shell=True) | |
912 | sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon)) | |
913 | sys.stderr.write("%s" % backtrace) | |
83c26937 | 914 | traces = traces + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s" % (self.name, daemon, backtrace) |
2a59a86b | 915 | reportMade = True |
f76774ec LB |
916 | elif reportLeaks: |
917 | log = self.getStdErr(daemon) | |
918 | if "memstats" in log: | |
919 | sys.stderr.write("%s: %s has memory leaks:\n" % (self.name, daemon)) | |
83c26937 | 920 | traces = traces + "\n%s: %s has memory leaks:\n" % (self.name, daemon) |
f76774ec LB |
921 | log = re.sub("core_handler: ", "", log) |
922 | log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n ## \1", log) | |
923 | log = re.sub("memstats: ", " ", log) | |
924 | sys.stderr.write(log) | |
2a59a86b | 925 | reportMade = True |
f76774ec LB |
926 | # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found |
927 | if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon): | |
928 | sys.stderr.write("%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon)) | |
83c26937 | 929 | traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon) |
2a59a86b LB |
930 | reportMade = True |
931 | if reportMade: | |
932 | self.reportCores = False | |
83c26937 | 933 | return traces |
f76774ec | 934 | |
594b1259 | 935 | def checkRouterRunning(self): |
597cabb7 MW |
936 | "Check if router daemons are running and collect crashinfo they don't run" |
937 | ||
594b1259 MW |
938 | global fatal_error |
939 | ||
940 | daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"') | |
4942f298 MW |
941 | # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found |
942 | if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"): | |
943 | return "%s: vtysh killed by AddressSanitizer" % (self.name) | |
944 | ||
594b1259 MW |
945 | for daemon in self.daemons: |
946 | if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning): | |
947 | sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon)) | |
d2132114 DS |
948 | if daemon is "staticd": |
949 | sys.stderr.write("You may have a copy of staticd installed but are attempting to test against\n") | |
950 | sys.stderr.write("a version of FRR that does not have staticd, please cleanup the install dir\n") | |
951 | ||
594b1259 | 952 | # Look for core file |
e1dfa45e | 953 | corefiles = glob.glob('{}/{}/{}_core*.dmp'.format( |
13e1fc49 | 954 | self.logdir, self.name, daemon)) |
594b1259 | 955 | if (len(corefiles) > 0): |
2ab85530 RZ |
956 | daemon_path = os.path.join(self.daemondir, daemon) |
957 | backtrace = subprocess.check_output([ | |
958 | "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0]) | |
959 | ], shell=True) | |
594b1259 MW |
960 | sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon)) |
961 | sys.stderr.write("%s\n" % backtrace) | |
962 | else: | |
963 | # No core found - If we find matching logfile in /tmp, then print last 20 lines from it. | |
e1dfa45e | 964 | if os.path.isfile('{}/{}/{}.log'.format(self.logdir, self.name, daemon)): |
13e1fc49 | 965 | log_tail = subprocess.check_output([ |
e1dfa45e | 966 | "tail -n20 {}/{}/{}.log 2> /dev/null".format( |
13e1fc49 RZ |
967 | self.logdir, self.name, daemon) |
968 | ], shell=True) | |
594b1259 MW |
969 | sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon)) |
970 | sys.stderr.write("%s\n" % log_tail) | |
4942f298 | 971 | |
597cabb7 | 972 | # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found |
4942f298 | 973 | if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon): |
84379e8e MW |
974 | return "%s: Daemon %s not running - killed by AddressSanitizer" % (self.name, daemon) |
975 | ||
594b1259 MW |
976 | return "%s: Daemon %s not running" % (self.name, daemon) |
977 | return "" | |
fb80b81b LB |
978 | |
979 | def checkRouterVersion(self, cmpop, version): | |
980 | """ | |
981 | Compares router version using operation `cmpop` with `version`. | |
982 | Valid `cmpop` values: | |
983 | * `>=`: has the same version or greater | |
984 | * '>': has greater version | |
985 | * '=': has the same version | |
986 | * '<': has a lesser version | |
987 | * '<=': has the same version or lesser | |
988 | ||
989 | Usage example: router.checkRouterVersion('>', '1.0') | |
990 | """ | |
6bfe4b8b MW |
991 | |
992 | # Make sure we have version information first | |
993 | if self.version == None: | |
994 | self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2] | |
995 | logger.info('{}: running version: {}'.format(self.name,self.version)) | |
996 | ||
fb80b81b LB |
997 | rversion = self.version |
998 | if rversion is None: | |
999 | return False | |
1000 | ||
1001 | result = version_cmp(rversion, version) | |
1002 | if cmpop == '>=': | |
1003 | return result >= 0 | |
1004 | if cmpop == '>': | |
1005 | return result > 0 | |
1006 | if cmpop == '=': | |
1007 | return result == 0 | |
1008 | if cmpop == '<': | |
1009 | return result < 0 | |
1010 | if cmpop == '<': | |
1011 | return result < 0 | |
1012 | if cmpop == '<=': | |
1013 | return result <= 0 | |
1014 | ||
594b1259 MW |
1015 | def get_ipv6_linklocal(self): |
1016 | "Get LinkLocal Addresses from interfaces" | |
1017 | ||
1018 | linklocal = [] | |
1019 | ||
1020 | ifaces = self.cmd('ip -6 address') | |
1021 | # Fix newlines (make them all the same) | |
1022 | ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines() | |
1023 | interface="" | |
1024 | ll_per_if_count=0 | |
1025 | for line in ifaces: | |
1026 | m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line) | |
1027 | if m: | |
1028 | interface = m.group(1) | |
1029 | ll_per_if_count = 0 | |
1030 | m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line) | |
1031 | if m: | |
1032 | local = m.group(1) | |
1033 | ll_per_if_count += 1 | |
1034 | if (ll_per_if_count > 1): | |
1035 | linklocal += [["%s-%s" % (interface, ll_per_if_count), local]] | |
1036 | else: | |
1037 | linklocal += [[interface, local]] | |
1038 | return linklocal | |
80eeefb7 MW |
1039 | def daemon_available(self, daemon): |
1040 | "Check if specified daemon is installed (and for ldp if kernel supports MPLS)" | |
1041 | ||
2ab85530 RZ |
1042 | daemon_path = os.path.join(self.daemondir, daemon) |
1043 | if not os.path.isfile(daemon_path): | |
80eeefb7 MW |
1044 | return False |
1045 | if (daemon == 'ldpd'): | |
b431b554 MW |
1046 | if version_cmp(platform.release(), '4.5') < 0: |
1047 | return False | |
f2d6ce41 | 1048 | if not module_present('mpls-router', load=False): |
80eeefb7 | 1049 | return False |
f2d6ce41 | 1050 | if not module_present('mpls-iptunnel', load=False): |
b431b554 | 1051 | return False |
80eeefb7 | 1052 | return True |
f2d6ce41 | 1053 | |
80eeefb7 MW |
1054 | def get_routertype(self): |
1055 | "Return the type of Router (frr or quagga)" | |
1056 | ||
1057 | return self.routertype | |
50c40bde MW |
1058 | def report_memory_leaks(self, filename_prefix, testscript): |
1059 | "Report Memory Leaks to file prefixed with given string" | |
1060 | ||
1061 | leakfound = False | |
1062 | filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt" | |
1063 | for daemon in self.daemons: | |
1064 | if (self.daemons[daemon] == 1): | |
1065 | log = self.getStdErr(daemon) | |
1066 | if "memstats" in log: | |
1067 | # Found memory leak | |
6c131bd3 RZ |
1068 | logger.info('\nRouter {} {} StdErr Log:\n{}'.format( |
1069 | self.name, daemon, log)) | |
50c40bde MW |
1070 | if not leakfound: |
1071 | leakfound = True | |
1072 | # Check if file already exists | |
1073 | fileexists = os.path.isfile(filename) | |
1074 | leakfile = open(filename, "a") | |
1075 | if not fileexists: | |
1076 | # New file - add header | |
1077 | leakfile.write("# Memory Leak Detection for topotest %s\n\n" % testscript) | |
1078 | leakfile.write("## Router %s\n" % self.name) | |
1079 | leakfile.write("### Process %s\n" % daemon) | |
1080 | log = re.sub("core_handler: ", "", log) | |
1081 | log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n#### \1\n", log) | |
1082 | log = re.sub("memstats: ", " ", log) | |
1083 | leakfile.write(log) | |
1084 | leakfile.write("\n") | |
1085 | if leakfound: | |
1086 | leakfile.close() | |
80eeefb7 | 1087 | |
7cc96035 | 1088 | class LinuxRouter(Router): |
4b1d6d4d | 1089 | "A Linux Router Node with IPv4/IPv6 forwarding enabled." |
7cc96035 DS |
1090 | |
1091 | def __init__(self, name, **params): | |
1092 | Router.__init__(self, name, **params) | |
1093 | ||
1094 | def config(self, **params): | |
1095 | Router.config(self, **params) | |
1096 | # Enable forwarding on the router | |
1097 | assert_sysctl(self, 'net.ipv4.ip_forward', 1) | |
1098 | assert_sysctl(self, 'net.ipv6.conf.all.forwarding', 1) | |
d29fb5bd DS |
1099 | # Enable coredumps |
1100 | assert_sysctl(self, 'kernel.core_uses_pid', 1) | |
1101 | assert_sysctl(self, 'fs.suid_dumpable', 1) | |
1102 | #this applies to the kernel not the namespace... | |
1103 | #original on ubuntu 17.x, but apport won't save as in namespace | |
1104 | # |/usr/share/apport/apport %p %s %c %d %P | |
1105 | corefile = '%e_core-sig_%s-pid_%p.dmp' | |
1106 | assert_sysctl(self, 'kernel.core_pattern', corefile) | |
1107 | ||
7cc96035 DS |
1108 | def terminate(self): |
1109 | """ | |
1110 | Terminate generic LinuxRouter Mininet instance | |
1111 | """ | |
1112 | set_sysctl(self, 'net.ipv4.ip_forward', 0) | |
1113 | set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0) | |
1114 | Router.terminate(self) | |
594b1259 | 1115 | |
4b1d6d4d DS |
1116 | class FreeBSDRouter(Router): |
1117 | "A FreeBSD Router Node with IPv4/IPv6 forwarding enabled." | |
1118 | ||
1119 | def __init__(eslf, name, **params): | |
1120 | Router.__init__(Self, name, **params) | |
1121 | ||
1122 | ||
594b1259 MW |
1123 | class LegacySwitch(OVSSwitch): |
1124 | "A Legacy Switch without OpenFlow" | |
1125 | ||
1126 | def __init__(self, name, **params): | |
1127 | OVSSwitch.__init__(self, name, failMode='standalone', **params) | |
1128 | self.switchIP = None |