]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topotest.py
Merge pull request #6437 from opensourcerouting/bfd-profiles-bgp
[mirror_frr.git] / tests / topotests / lib / topotest.py
CommitLineData
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 25import json
594b1259 26import os
50c40bde 27import errno
594b1259
MW
28import re
29import sys
fd858290 30import functools
594b1259
MW
31import glob
32import StringIO
33import subprocess
1fca63c1 34import tempfile
594b1259 35import platform
17070436 36import difflib
570f25d8 37import time
594b1259 38
6c131bd3 39from lib.topolog import logger
849224d4 40from copy import deepcopy
6c131bd3 41
04ce2b97
RZ
42if sys.version_info[0] > 2:
43 import configparser
44else:
45 import ConfigParser as configparser
46
594b1259
MW
47from mininet.topo import Topo
48from mininet.net import Mininet
49from mininet.node import Node, OVSSwitch, Host
50from mininet.log import setLogLevel, info
51from mininet.cli import CLI
52from mininet.link import Intf
53
787e7624 54
3668ed8d
RZ
55class json_cmp_result(object):
56 "json_cmp result class for better assertion messages"
57
58 def __init__(self):
59 self.errors = []
60
61 def add_error(self, error):
62 "Append error message to the result"
2db5888d
RZ
63 for line in error.splitlines():
64 self.errors.append(line)
3668ed8d
RZ
65
66 def has_errors(self):
67 "Returns True if there were errors, otherwise False."
68 return len(self.errors) > 0
69
849224d4
G
70 def gen_report(self):
71 headline = ["Generated JSON diff error report:", ""]
72 return headline + self.errors
73
7fe06d55 74 def __str__(self):
849224d4
G
75 return (
76 "Generated JSON diff error report:\n\n\n" + "\n".join(self.errors) + "\n\n"
77 )
7fe06d55 78
da63d5b3 79
849224d4 80def gen_json_diff_report(d1, d2, exact=False, path="> $", acc=(0, "")):
7bd28cfc 81 """
849224d4 82 Internal workhorse which compares two JSON data structures and generates an error report suited to be read by a human eye.
7bd28cfc 83 """
849224d4
G
84
85 def dump_json(v):
86 if isinstance(v, (dict, list)):
87 return "\t" + "\t".join(
88 json.dumps(v, indent=4, separators=(",", ": ")).splitlines(True)
787e7624 89 )
849224d4
G
90 else:
91 return "'{}'".format(v)
92
93 def json_type(v):
94 if isinstance(v, (list, tuple)):
95 return "Array"
96 elif isinstance(v, dict):
97 return "Object"
98 elif isinstance(v, (int, float)):
99 return "Number"
100 elif isinstance(v, bool):
101 return "Boolean"
102 elif isinstance(v, str):
103 return "String"
104 elif v == None:
105 return "null"
106
107 def get_errors(other_acc):
108 return other_acc[1]
109
110 def get_errors_n(other_acc):
111 return other_acc[0]
112
113 def add_error(acc, msg, points=1):
114 return (acc[0] + points, acc[1] + "{}: {}\n".format(path, msg))
115
116 def merge_errors(acc, other_acc):
117 return (acc[0] + other_acc[0], acc[1] + other_acc[1])
118
119 def add_idx(idx):
120 return "{}[{}]".format(path, idx)
121
122 def add_key(key):
123 return "{}->{}".format(path, key)
124
125 def has_errors(other_acc):
126 return other_acc[0] > 0
127
128 if d2 == "*" or (
129 not isinstance(d1, (list, dict))
130 and not isinstance(d2, (list, dict))
131 and d1 == d2
132 ):
133 return acc
134 elif (
135 not isinstance(d1, (list, dict))
136 and not isinstance(d2, (list, dict))
137 and d1 != d2
138 ):
139 acc = add_error(
140 acc,
141 "d1 has element with value '{}' but in d2 it has value '{}'".format(d1, d2),
787e7624 142 )
849224d4
G
143 elif (
144 isinstance(d1, list)
145 and isinstance(d2, list)
146 and ((len(d2) > 0 and d2[0] == "__ordered__") or exact)
147 ):
148 if not exact:
149 del d2[0]
150 if len(d1) != len(d2):
151 acc = add_error(
152 acc,
153 "d1 has Array of length {} but in d2 it is of length {}".format(
154 len(d1), len(d2)
155 ),
787e7624 156 )
849224d4
G
157 else:
158 for idx, v1, v2 in zip(range(0, len(d1)), d1, d2):
159 acc = merge_errors(
160 acc, gen_json_diff_report(v1, v2, exact=exact, path=add_idx(idx))
161 )
162 elif isinstance(d1, list) and isinstance(d2, list):
163 if len(d1) < len(d2):
164 acc = add_error(
165 acc,
166 "d1 has Array of length {} but in d2 it is of length {}".format(
167 len(d1), len(d2)
168 ),
169 )
170 else:
171 for idx2, v2 in zip(range(0, len(d2)), d2):
172 found_match = False
173 closest_diff = None
174 closest_idx = None
175 for idx1, v1 in zip(range(0, len(d1)), d1):
b3100f6c
G
176 tmp_v1 = deepcopy(v1)
177 tmp_v2 = deepcopy(v2)
178 tmp_diff = gen_json_diff_report(tmp_v1, tmp_v2, path=add_idx(idx1))
849224d4
G
179 if not has_errors(tmp_diff):
180 found_match = True
181 del d1[idx1]
182 break
183 elif not closest_diff or get_errors_n(tmp_diff) < get_errors_n(
184 closest_diff
185 ):
186 closest_diff = tmp_diff
187 closest_idx = idx1
188 if not found_match and isinstance(v2, (list, dict)):
189 sub_error = "\n\n\t{}".format(
190 "\t".join(get_errors(closest_diff).splitlines(True))
191 )
192 acc = add_error(
193 acc,
194 (
195 "d2 has the following element at index {} which is not present in d1: "
196 + "\n\n{}\n\n\tClosest match in d1 is at index {} with the following errors: {}"
197 ).format(idx2, dump_json(v2), closest_idx, sub_error),
198 )
199 if not found_match and not isinstance(v2, (list, dict)):
200 acc = add_error(
201 acc,
202 "d2 has the following element at index {} which is not present in d1: {}".format(
203 idx2, dump_json(v2)
204 ),
205 )
206 elif isinstance(d1, dict) and isinstance(d2, dict) and exact:
207 invalid_keys_d1 = [k for k in d1.keys() if k not in d2.keys()]
208 invalid_keys_d2 = [k for k in d2.keys() if k not in d1.keys()]
209 for k in invalid_keys_d1:
210 acc = add_error(acc, "d1 has key '{}' which is not present in d2".format(k))
211 for k in invalid_keys_d2:
212 acc = add_error(acc, "d2 has key '{}' which is not present in d1".format(k))
213 valid_keys_intersection = [k for k in d1.keys() if k in d2.keys()]
214 for k in valid_keys_intersection:
215 acc = merge_errors(
216 acc, gen_json_diff_report(d1[k], d2[k], exact=exact, path=add_key(k))
217 )
218 elif isinstance(d1, dict) and isinstance(d2, dict):
219 none_keys = [k for k, v in d2.items() if v == None]
220 none_keys_present = [k for k in d1.keys() if k in none_keys]
221 for k in none_keys_present:
222 acc = add_error(
223 acc, "d1 has key '{}' which is not supposed to be present".format(k)
224 )
225 keys = [k for k, v in d2.items() if v != None]
226 invalid_keys_intersection = [k for k in keys if k not in d1.keys()]
227 for k in invalid_keys_intersection:
228 acc = add_error(acc, "d2 has key '{}' which is not present in d1".format(k))
229 valid_keys_intersection = [k for k in keys if k in d1.keys()]
230 for k in valid_keys_intersection:
231 acc = merge_errors(
232 acc, gen_json_diff_report(d1[k], d2[k], exact=exact, path=add_key(k))
233 )
234 else:
235 acc = add_error(
236 acc,
237 "d1 has element of type '{}' but the corresponding element in d2 is of type '{}'".format(
238 json_type(d1), json_type(d2)
239 ),
240 points=2,
787e7624 241 )
a82e5f9a 242
849224d4 243 return acc
a82e5f9a 244
849224d4
G
245
246def json_cmp(d1, d2, exact=False):
09e21b44
RZ
247 """
248 JSON compare function. Receives two parameters:
849224d4
G
249 * `d1`: parsed JSON data structure
250 * `d2`: parsed JSON data structure
251
252 Returns 'None' when all JSON Object keys and all Array elements of d2 have a match
253 in d1, e.g. when d2 is a "subset" of d1 without honoring any order. Otherwise an
254 error report is generated and wrapped in a 'json_cmp_result()'. There are special
255 parameters and notations explained below which can be used to cover rather unusual
256 cases:
257
258 * when 'exact is set to 'True' then d1 and d2 are tested for equality (including
259 order within JSON Arrays)
260 * using 'null' (or 'None' in Python) as JSON Object value is checking for key
261 absence in d1
262 * using '*' as JSON Object value or Array value is checking for presence in d1
263 without checking the values
264 * using '__ordered__' as first element in a JSON Array in d2 will also check the
265 order when it is compared to an Array in d1
09e21b44 266 """
09e21b44 267
849224d4 268 (errors_n, errors) = gen_json_diff_report(deepcopy(d1), deepcopy(d2), exact=exact)
3668ed8d 269
849224d4
G
270 if errors_n > 0:
271 result = json_cmp_result()
272 result.add_error(errors)
3668ed8d 273 return result
849224d4
G
274 else:
275 return None
09e21b44 276
a82e5f9a 277
5cffda18
RZ
278def router_output_cmp(router, cmd, expected):
279 """
280 Runs `cmd` in router and compares the output with `expected`.
281 """
787e7624 282 return difflines(
283 normalize_text(router.vtysh_cmd(cmd)),
284 normalize_text(expected),
285 title1="Current output",
286 title2="Expected output",
287 )
5cffda18
RZ
288
289
849224d4 290def router_json_cmp(router, cmd, data, exact=False):
5cffda18
RZ
291 """
292 Runs `cmd` that returns JSON data (normally the command ends with 'json')
293 and compare with `data` contents.
294 """
849224d4 295 return json_cmp(router.vtysh_cmd(cmd, isjson=True), data, exact)
5cffda18
RZ
296
297
1fca63c1
RZ
298def run_and_expect(func, what, count=20, wait=3):
299 """
300 Run `func` and compare the result with `what`. Do it for `count` times
301 waiting `wait` seconds between tries. By default it tries 20 times with
302 3 seconds delay between tries.
303
304 Returns (True, func-return) on success or
305 (False, func-return) on failure.
5cffda18
RZ
306
307 ---
308
309 Helper functions to use with this function:
310 - router_output_cmp
311 - router_json_cmp
1fca63c1 312 """
fd858290
RZ
313 start_time = time.time()
314 func_name = "<unknown>"
315 if func.__class__ == functools.partial:
316 func_name = func.func.__name__
317 else:
318 func_name = func.__name__
319
320 logger.info(
321 "'{}' polling started (interval {} secs, maximum wait {} secs)".format(
787e7624 322 func_name, wait, int(wait * count)
323 )
324 )
fd858290 325
1fca63c1
RZ
326 while count > 0:
327 result = func()
328 if result != what:
570f25d8 329 time.sleep(wait)
1fca63c1
RZ
330 count -= 1
331 continue
fd858290
RZ
332
333 end_time = time.time()
787e7624 334 logger.info(
335 "'{}' succeeded after {:.2f} seconds".format(
336 func_name, end_time - start_time
337 )
338 )
1fca63c1 339 return (True, result)
fd858290
RZ
340
341 end_time = time.time()
787e7624 342 logger.error(
343 "'{}' failed after {:.2f} seconds".format(func_name, end_time - start_time)
344 )
1fca63c1
RZ
345 return (False, result)
346
347
a6fd124a
RZ
348def run_and_expect_type(func, etype, count=20, wait=3, avalue=None):
349 """
350 Run `func` and compare the result with `etype`. Do it for `count` times
351 waiting `wait` seconds between tries. By default it tries 20 times with
352 3 seconds delay between tries.
353
354 This function is used when you want to test the return type and,
355 optionally, the return value.
356
357 Returns (True, func-return) on success or
358 (False, func-return) on failure.
359 """
360 start_time = time.time()
361 func_name = "<unknown>"
362 if func.__class__ == functools.partial:
363 func_name = func.func.__name__
364 else:
365 func_name = func.__name__
366
367 logger.info(
368 "'{}' polling started (interval {} secs, maximum wait {} secs)".format(
787e7624 369 func_name, wait, int(wait * count)
370 )
371 )
a6fd124a
RZ
372
373 while count > 0:
374 result = func()
375 if not isinstance(result, etype):
787e7624 376 logger.debug(
377 "Expected result type '{}' got '{}' instead".format(etype, type(result))
378 )
a6fd124a
RZ
379 time.sleep(wait)
380 count -= 1
381 continue
382
383 if etype != type(None) and avalue != None and result != avalue:
384 logger.debug("Expected value '{}' got '{}' instead".format(avalue, result))
385 time.sleep(wait)
386 count -= 1
387 continue
388
389 end_time = time.time()
787e7624 390 logger.info(
391 "'{}' succeeded after {:.2f} seconds".format(
392 func_name, end_time - start_time
393 )
394 )
a6fd124a
RZ
395 return (True, result)
396
397 end_time = time.time()
787e7624 398 logger.error(
399 "'{}' failed after {:.2f} seconds".format(func_name, end_time - start_time)
400 )
a6fd124a
RZ
401 return (False, result)
402
403
594b1259
MW
404def int2dpid(dpid):
405 "Converting Integer to DPID"
406
407 try:
408 dpid = hex(dpid)[2:]
787e7624 409 dpid = "0" * (16 - len(dpid)) + dpid
594b1259
MW
410 return dpid
411 except IndexError:
787e7624 412 raise Exception(
413 "Unable to derive default datapath ID - "
414 "please either specify a dpid or use a "
415 "canonical switch name such as s23."
416 )
417
594b1259 418
50c40bde
MW
419def pid_exists(pid):
420 "Check whether pid exists in the current process table."
421
422 if pid <= 0:
423 return False
424 try:
425 os.kill(pid, 0)
426 except OSError as err:
427 if err.errno == errno.ESRCH:
428 # ESRCH == No such process
429 return False
430 elif err.errno == errno.EPERM:
431 # EPERM clearly means there's a process to deny access to
432 return True
433 else:
434 # According to "man 2 kill" possible error values are
435 # (EINVAL, EPERM, ESRCH)
436 raise
437 else:
438 return True
439
787e7624 440
bc2872fd 441def get_textdiff(text1, text2, title1="", title2="", **opts):
17070436
MW
442 "Returns empty string if same or formatted diff"
443
787e7624 444 diff = "\n".join(
445 difflib.unified_diff(text1, text2, fromfile=title1, tofile=title2, **opts)
446 )
17070436
MW
447 # Clean up line endings
448 diff = os.linesep.join([s for s in diff.splitlines() if s])
449 return diff
450
787e7624 451
452def difflines(text1, text2, title1="", title2="", **opts):
1fca63c1 453 "Wrapper for get_textdiff to avoid string transformations."
787e7624 454 text1 = ("\n".join(text1.rstrip().splitlines()) + "\n").splitlines(1)
455 text2 = ("\n".join(text2.rstrip().splitlines()) + "\n").splitlines(1)
bc2872fd 456 return get_textdiff(text1, text2, title1, title2, **opts)
1fca63c1 457
787e7624 458
1fca63c1
RZ
459def get_file(content):
460 """
461 Generates a temporary file in '/tmp' with `content` and returns the file name.
462 """
787e7624 463 fde = tempfile.NamedTemporaryFile(mode="w", delete=False)
1fca63c1
RZ
464 fname = fde.name
465 fde.write(content)
466 fde.close()
467 return fname
468
787e7624 469
f7840f6b
RZ
470def normalize_text(text):
471 """
9683a1bb 472 Strips formating spaces/tabs, carriage returns and trailing whitespace.
f7840f6b 473 """
787e7624 474 text = re.sub(r"[ \t]+", " ", text)
475 text = re.sub(r"\r", "", text)
9683a1bb
RZ
476
477 # Remove whitespace in the middle of text.
787e7624 478 text = re.sub(r"[ \t]+\n", "\n", text)
9683a1bb
RZ
479 # Remove whitespace at the end of the text.
480 text = text.rstrip()
481
f7840f6b
RZ
482 return text
483
787e7624 484
cc95fbd9 485def module_present_linux(module, load):
f2d6ce41
CF
486 """
487 Returns whether `module` is present.
488
489 If `load` is true, it will try to load it via modprobe.
490 """
787e7624 491 with open("/proc/modules", "r") as modules_file:
492 if module.replace("-", "_") in modules_file.read():
f2d6ce41 493 return True
787e7624 494 cmd = "/sbin/modprobe {}{}".format("" if load else "-n ", module)
f2d6ce41
CF
495 if os.system(cmd) != 0:
496 return False
497 else:
498 return True
499
787e7624 500
cc95fbd9
DS
501def module_present_freebsd(module, load):
502 return True
503
787e7624 504
cc95fbd9
DS
505def module_present(module, load=True):
506 if sys.platform.startswith("linux"):
28440fd9 507 return module_present_linux(module, load)
cc95fbd9 508 elif sys.platform.startswith("freebsd"):
28440fd9 509 return module_present_freebsd(module, load)
cc95fbd9 510
787e7624 511
4190fe1e
RZ
512def version_cmp(v1, v2):
513 """
514 Compare two version strings and returns:
515
516 * `-1`: if `v1` is less than `v2`
517 * `0`: if `v1` is equal to `v2`
518 * `1`: if `v1` is greater than `v2`
519
520 Raises `ValueError` if versions are not well formated.
521 """
787e7624 522 vregex = r"(?P<whole>\d+(\.(\d+))*)"
4190fe1e
RZ
523 v1m = re.match(vregex, v1)
524 v2m = re.match(vregex, v2)
525 if v1m is None or v2m is None:
526 raise ValueError("got a invalid version string")
527
528 # Split values
787e7624 529 v1g = v1m.group("whole").split(".")
530 v2g = v2m.group("whole").split(".")
4190fe1e
RZ
531
532 # Get the longest version string
533 vnum = len(v1g)
534 if len(v2g) > vnum:
535 vnum = len(v2g)
536
537 # Reverse list because we are going to pop the tail
538 v1g.reverse()
539 v2g.reverse()
540 for _ in range(vnum):
541 try:
542 v1n = int(v1g.pop())
543 except IndexError:
544 while v2g:
545 v2n = int(v2g.pop())
546 if v2n > 0:
547 return -1
548 break
549
550 try:
551 v2n = int(v2g.pop())
552 except IndexError:
553 if v1n > 0:
554 return 1
555 while v1g:
556 v1n = int(v1g.pop())
557 if v1n > 0:
034237db 558 return 1
4190fe1e
RZ
559 break
560
561 if v1n > v2n:
562 return 1
563 if v1n < v2n:
564 return -1
565 return 0
566
787e7624 567
f5612168
PG
568def interface_set_status(node, ifacename, ifaceaction=False, vrf_name=None):
569 if ifaceaction:
787e7624 570 str_ifaceaction = "no shutdown"
f5612168 571 else:
787e7624 572 str_ifaceaction = "shutdown"
f5612168 573 if vrf_name == None:
787e7624 574 cmd = 'vtysh -c "configure terminal" -c "interface {0}" -c "{1}"'.format(
575 ifacename, str_ifaceaction
576 )
f5612168 577 else:
787e7624 578 cmd = 'vtysh -c "configure terminal" -c "interface {0} vrf {1}" -c "{2}"'.format(
579 ifacename, vrf_name, str_ifaceaction
580 )
f5612168
PG
581 node.run(cmd)
582
787e7624 583
b220b3c8
PG
584def ip4_route_zebra(node, vrf_name=None):
585 """
586 Gets an output of 'show ip route' command. It can be used
587 with comparing the output to a reference
588 """
589 if vrf_name == None:
787e7624 590 tmp = node.vtysh_cmd("show ip route")
b220b3c8 591 else:
787e7624 592 tmp = node.vtysh_cmd("show ip route vrf {0}".format(vrf_name))
b220b3c8 593 output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp)
41077aa1
CF
594
595 lines = output.splitlines()
596 header_found = False
0eff5820 597 while lines and (not lines[0].strip() or not header_found):
787e7624 598 if "> - selected route" in lines[0]:
41077aa1
CF
599 header_found = True
600 lines = lines[1:]
787e7624 601 return "\n".join(lines)
602
b220b3c8 603
e394d9aa
MS
604def ip6_route_zebra(node, vrf_name=None):
605 """
606 Retrieves the output of 'show ipv6 route [vrf vrf_name]', then
607 canonicalizes it by eliding link-locals.
608 """
609
610 if vrf_name == None:
787e7624 611 tmp = node.vtysh_cmd("show ipv6 route")
e394d9aa 612 else:
787e7624 613 tmp = node.vtysh_cmd("show ipv6 route vrf {0}".format(vrf_name))
e394d9aa
MS
614
615 # Mask out timestamp
616 output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp)
617
618 # Mask out the link-local addresses
787e7624 619 output = re.sub(r"fe80::[^ ]+,", "fe80::XXXX:XXXX:XXXX:XXXX,", output)
e394d9aa
MS
620
621 lines = output.splitlines()
622 header_found = False
623 while lines and (not lines[0].strip() or not header_found):
787e7624 624 if "> - selected route" in lines[0]:
e394d9aa
MS
625 header_found = True
626 lines = lines[1:]
627
787e7624 628 return "\n".join(lines)
e394d9aa
MS
629
630
2f726781
MW
631def proto_name_to_number(protocol):
632 return {
787e7624 633 "bgp": "186",
634 "isis": "187",
635 "ospf": "188",
636 "rip": "189",
637 "ripng": "190",
638 "nhrp": "191",
639 "eigrp": "192",
640 "ldp": "193",
641 "sharp": "194",
642 "pbr": "195",
643 "static": "196",
644 }.get(
645 protocol, protocol
646 ) # default return same as input
2f726781
MW
647
648
99a7a912
RZ
649def ip4_route(node):
650 """
651 Gets a structured return of the command 'ip route'. It can be used in
652 conjuction with json_cmp() to provide accurate assert explanations.
653
654 Return example:
655 {
656 '10.0.1.0/24': {
657 'dev': 'eth0',
658 'via': '172.16.0.1',
659 'proto': '188',
660 },
661 '10.0.2.0/24': {
662 'dev': 'eth1',
663 'proto': 'kernel',
664 }
665 }
666 """
787e7624 667 output = normalize_text(node.run("ip route")).splitlines()
99a7a912
RZ
668 result = {}
669 for line in output:
787e7624 670 columns = line.split(" ")
99a7a912
RZ
671 route = result[columns[0]] = {}
672 prev = None
673 for column in columns:
787e7624 674 if prev == "dev":
675 route["dev"] = column
676 if prev == "via":
677 route["via"] = column
678 if prev == "proto":
2f726781 679 # translate protocol names back to numbers
787e7624 680 route["proto"] = proto_name_to_number(column)
681 if prev == "metric":
682 route["metric"] = column
683 if prev == "scope":
684 route["scope"] = column
99a7a912
RZ
685 prev = column
686
687 return result
688
787e7624 689
99a7a912
RZ
690def ip6_route(node):
691 """
692 Gets a structured return of the command 'ip -6 route'. It can be used in
693 conjuction with json_cmp() to provide accurate assert explanations.
694
695 Return example:
696 {
697 '2001:db8:1::/64': {
698 'dev': 'eth0',
699 'proto': '188',
700 },
701 '2001:db8:2::/64': {
702 'dev': 'eth1',
703 'proto': 'kernel',
704 }
705 }
706 """
787e7624 707 output = normalize_text(node.run("ip -6 route")).splitlines()
99a7a912
RZ
708 result = {}
709 for line in output:
787e7624 710 columns = line.split(" ")
99a7a912
RZ
711 route = result[columns[0]] = {}
712 prev = None
713 for column in columns:
787e7624 714 if prev == "dev":
715 route["dev"] = column
716 if prev == "via":
717 route["via"] = column
718 if prev == "proto":
2f726781 719 # translate protocol names back to numbers
787e7624 720 route["proto"] = proto_name_to_number(column)
721 if prev == "metric":
722 route["metric"] = column
723 if prev == "pref":
724 route["pref"] = column
99a7a912
RZ
725 prev = column
726
727 return result
728
787e7624 729
9b7decf2
JU
730def ip_rules(node):
731 """
732 Gets a structured return of the command 'ip rule'. It can be used in
733 conjuction with json_cmp() to provide accurate assert explanations.
734
735 Return example:
736 [
737 {
738 "pref": "0"
739 "from": "all"
740 },
741 {
742 "pref": "32766"
743 "from": "all"
744 },
745 {
746 "to": "3.4.5.0/24",
747 "iif": "r1-eth2",
748 "pref": "304",
749 "from": "1.2.0.0/16",
750 "proto": "zebra"
751 }
752 ]
753 """
754 output = normalize_text(node.run("ip rule")).splitlines()
755 result = []
756 for line in output:
757 columns = line.split(" ")
758
759 route = {}
760 # remove last character, since it is ':'
761 pref = columns[0][:-1]
762 route["pref"] = pref
763 prev = None
764 for column in columns:
765 if prev == "from":
766 route["from"] = column
767 if prev == "to":
768 route["to"] = column
769 if prev == "proto":
770 route["proto"] = column
771 if prev == "iif":
772 route["iif"] = column
773 if prev == "fwmark":
774 route["fwmark"] = column
775 prev = column
776
777 result.append(route)
778 return result
779
780
570f25d8
RZ
781def sleep(amount, reason=None):
782 """
783 Sleep wrapper that registers in the log the amount of sleep
784 """
785 if reason is None:
787e7624 786 logger.info("Sleeping for {} seconds".format(amount))
570f25d8 787 else:
787e7624 788 logger.info(reason + " ({} seconds)".format(amount))
570f25d8
RZ
789
790 time.sleep(amount)
791
787e7624 792
4942f298
MW
793def checkAddressSanitizerError(output, router, component):
794 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
795
787e7624 796 addressSantizerError = re.search(
797 "(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ", output
798 )
4942f298 799 if addressSantizerError:
787e7624 800 sys.stderr.write(
801 "%s: %s triggered an exception by AddressSanitizer\n" % (router, component)
802 )
4942f298
MW
803 # Sanitizer Error found in log
804 pidMark = addressSantizerError.group(1)
787e7624 805 addressSantizerLog = re.search(
806 "%s(.*)%s" % (pidMark, pidMark), output, re.DOTALL
807 )
4942f298 808 if addressSantizerLog:
787e7624 809 callingTest = os.path.basename(
810 sys._current_frames().values()[0].f_back.f_back.f_globals["__file__"]
811 )
4942f298
MW
812 callingProc = sys._getframe(2).f_code.co_name
813 with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile:
787e7624 814 sys.stderr.write(
815 "\n".join(addressSantizerLog.group(1).splitlines()) + "\n"
816 )
4942f298 817 addrSanFile.write("## Error: %s\n\n" % addressSantizerError.group(2))
787e7624 818 addrSanFile.write(
819 "### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n"
820 % (callingTest, callingProc, router)
821 )
822 addrSanFile.write(
823 " "
824 + "\n ".join(addressSantizerLog.group(1).splitlines())
825 + "\n"
826 )
4942f298
MW
827 addrSanFile.write("\n---------------\n")
828 return True
6c131bd3 829 return False
4942f298 830
787e7624 831
594b1259 832def addRouter(topo, name):
80eeefb7 833 "Adding a FRRouter (or Quagga) to Topology"
594b1259 834
787e7624 835 MyPrivateDirs = [
836 "/etc/frr",
837 "/etc/quagga",
838 "/var/run/frr",
839 "/var/run/quagga",
840 "/var/log",
841 ]
4b1d6d4d
DS
842 if sys.platform.startswith("linux"):
843 return topo.addNode(name, cls=LinuxRouter, privateDirs=MyPrivateDirs)
844 elif sys.platform.startswith("freebsd"):
845 return topo.addNode(name, cls=FreeBSDRouter, privateDirs=MyPrivateDirs)
594b1259 846
787e7624 847
797e8dcf
RZ
848def set_sysctl(node, sysctl, value):
849 "Set a sysctl value and return None on success or an error string"
787e7624 850 valuestr = "{}".format(value)
797e8dcf
RZ
851 command = "sysctl {0}={1}".format(sysctl, valuestr)
852 cmdret = node.cmd(command)
853
787e7624 854 matches = re.search(r"([^ ]+) = ([^\s]+)", cmdret)
797e8dcf
RZ
855 if matches is None:
856 return cmdret
857 if matches.group(1) != sysctl:
858 return cmdret
859 if matches.group(2) != valuestr:
860 return cmdret
861
862 return None
863
787e7624 864
797e8dcf
RZ
865def assert_sysctl(node, sysctl, value):
866 "Set and assert that the sysctl is set with the specified value."
867 assert set_sysctl(node, sysctl, value) is None
868
594b1259
MW
869
870class Router(Node):
871 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
872
2ab85530
RZ
873 def __init__(self, name, **params):
874 super(Router, self).__init__(name, **params)
787e7624 875 self.logdir = params.get("logdir")
0d5e41c6 876
04ce2b97
RZ
877 # Backward compatibility:
878 # Load configuration defaults like topogen.
787e7624 879 self.config_defaults = configparser.ConfigParser(
880 {
881 "verbosity": "info",
882 "frrdir": "/usr/lib/frr",
883 "quaggadir": "/usr/lib/quagga",
884 "routertype": "frr",
885 "memleak_path": None,
886 }
887 )
04ce2b97 888 self.config_defaults.read(
787e7624 889 os.path.join(os.path.dirname(os.path.realpath(__file__)), "../pytest.ini")
04ce2b97
RZ
890 )
891
0d5e41c6
RZ
892 # If this topology is using old API and doesn't have logdir
893 # specified, then attempt to generate an unique logdir.
894 if self.logdir is None:
787e7624 895 cur_test = os.environ["PYTEST_CURRENT_TEST"]
896 self.logdir = "/tmp/topotests/" + cur_test[
897 0 : cur_test.find(".py")
898 ].replace("/", ".")
0d5e41c6
RZ
899
900 # If the logdir is not created, then create it and set the
901 # appropriated permissions.
902 if not os.path.isdir(self.logdir):
787e7624 903 os.system("mkdir -p " + self.logdir + "/" + name)
904 os.system("chmod -R go+rw /tmp/topotests")
0d5e41c6 905
2ab85530 906 self.daemondir = None
447f2d5a 907 self.hasmpls = False
787e7624 908 self.routertype = "frr"
909 self.daemons = {
910 "zebra": 0,
911 "ripd": 0,
912 "ripngd": 0,
913 "ospfd": 0,
914 "ospf6d": 0,
915 "isisd": 0,
916 "bgpd": 0,
917 "pimd": 0,
918 "ldpd": 0,
919 "eigrpd": 0,
920 "nhrpd": 0,
921 "staticd": 0,
922 "bfdd": 0,
923 "sharpd": 0,
a0764a36 924 "babeld": 0,
223f87f4 925 "pbrd": 0,
787e7624 926 }
927 self.daemons_options = {"zebra": ""}
2a59a86b 928 self.reportCores = True
fb80b81b 929 self.version = None
2ab85530 930
edd2bdf6
RZ
931 def _config_frr(self, **params):
932 "Configure FRR binaries"
787e7624 933 self.daemondir = params.get("frrdir")
edd2bdf6 934 if self.daemondir is None:
787e7624 935 self.daemondir = self.config_defaults.get("topogen", "frrdir")
edd2bdf6 936
787e7624 937 zebra_path = os.path.join(self.daemondir, "zebra")
edd2bdf6
RZ
938 if not os.path.isfile(zebra_path):
939 raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path))
940
941 def _config_quagga(self, **params):
942 "Configure Quagga binaries"
787e7624 943 self.daemondir = params.get("quaggadir")
edd2bdf6 944 if self.daemondir is None:
787e7624 945 self.daemondir = self.config_defaults.get("topogen", "quaggadir")
edd2bdf6 946
787e7624 947 zebra_path = os.path.join(self.daemondir, "zebra")
edd2bdf6 948 if not os.path.isfile(zebra_path):
787e7624 949 raise Exception(
950 "Quagga zebra binary doesn't exist at {}".format(zebra_path)
951 )
edd2bdf6 952
2ab85530
RZ
953 # pylint: disable=W0221
954 # Some params are only meaningful for the parent class.
594b1259
MW
955 def config(self, **params):
956 super(Router, self).config(**params)
957
2ab85530 958 # User did not specify the daemons directory, try to autodetect it.
787e7624 959 self.daemondir = params.get("daemondir")
2ab85530 960 if self.daemondir is None:
787e7624 961 self.routertype = params.get(
962 "routertype", self.config_defaults.get("topogen", "routertype")
963 )
964 if self.routertype == "quagga":
edd2bdf6
RZ
965 self._config_quagga(**params)
966 else:
967 self._config_frr(**params)
594b1259 968 else:
2ab85530 969 # Test the provided path
787e7624 970 zpath = os.path.join(self.daemondir, "zebra")
2ab85530 971 if not os.path.isfile(zpath):
787e7624 972 raise Exception("No zebra binary found in {}".format(zpath))
2ab85530 973 # Allow user to specify routertype when the path was specified.
787e7624 974 if params.get("routertype") is not None:
975 self.routertype = params.get("routertype")
2ab85530 976
787e7624 977 self.cmd("ulimit -c unlimited")
594b1259 978 # Set ownership of config files
787e7624 979 self.cmd("chown {0}:{0}vty /etc/{0}".format(self.routertype))
2ab85530 980
594b1259
MW
981 def terminate(self):
982 # Delete Running Quagga or FRR Daemons
99561211
MW
983 self.stopRouter()
984 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
985 # for d in StringIO.StringIO(rundaemons):
986 # self.cmd('kill -7 `cat %s`' % d.rstrip())
987 # self.waitOutput()
594b1259 988 # Disable forwarding
787e7624 989 set_sysctl(self, "net.ipv4.ip_forward", 0)
990 set_sysctl(self, "net.ipv6.conf.all.forwarding", 0)
594b1259 991 super(Router, self).terminate()
787e7624 992 os.system("chmod -R go+rw /tmp/topotests")
b0f0d980 993
787e7624 994 def stopRouter(self, wait=True, assertOnError=True, minErrorVersion="5.1"):
99561211 995 # Stop Running Quagga or FRR Daemons
787e7624 996 rundaemons = self.cmd("ls -1 /var/run/%s/*.pid" % self.routertype)
83c26937 997 errors = ""
e600b2d9 998 if re.search(r"No such file or directory", rundaemons):
83c26937 999 return errors
99561211 1000 if rundaemons is not None:
3a568b9c 1001 numRunning = 0
7551168c 1002 for d in StringIO.StringIO(rundaemons):
787e7624 1003 daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip()
1004 if daemonpid.isdigit() and pid_exists(int(daemonpid)):
1005 logger.info(
1006 "{}: stopping {}".format(
1007 self.name, os.path.basename(d.rstrip().rsplit(".", 1)[0])
1008 )
1009 )
1010 self.cmd("kill -TERM %s" % daemonpid)
7551168c 1011 self.waitOutput()
3a568b9c
LB
1012 if pid_exists(int(daemonpid)):
1013 numRunning += 1
1014 if wait and numRunning > 0:
787e7624 1015 sleep(2, "{}: waiting for daemons stopping".format(self.name))
3a568b9c
LB
1016 # 2nd round of kill if daemons didn't exit
1017 for d in StringIO.StringIO(rundaemons):
787e7624 1018 daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip()
1019 if daemonpid.isdigit() and pid_exists(int(daemonpid)):
1020 logger.info(
1021 "{}: killing {}".format(
1022 self.name,
1023 os.path.basename(d.rstrip().rsplit(".", 1)[0]),
1024 )
1025 )
1026 self.cmd("kill -7 %s" % daemonpid)
3a568b9c 1027 self.waitOutput()
787e7624 1028 self.cmd("rm -- {}".format(d.rstrip()))
f76774ec 1029 if wait:
787e7624 1030 errors = self.checkRouterCores(reportOnce=True)
1031 if self.checkRouterVersion("<", minErrorVersion):
1032 # ignore errors in old versions
1033 errors = ""
1034 if assertOnError and len(errors) > 0:
1035 assert "Errors found - details follow:" == 0, errors
83c26937 1036 return errors
f76774ec 1037
594b1259
MW
1038 def removeIPs(self):
1039 for interface in self.intfNames():
787e7624 1040 self.cmd("ip address flush", interface)
8dd5077d
PG
1041
1042 def checkCapability(self, daemon, param):
1043 if param is not None:
1044 daemon_path = os.path.join(self.daemondir, daemon)
787e7624 1045 daemon_search_option = param.replace("-", "")
1046 output = self.cmd(
1047 "{0} -h | grep {1}".format(daemon_path, daemon_search_option)
1048 )
8dd5077d
PG
1049 if daemon_search_option not in output:
1050 return False
1051 return True
1052
1053 def loadConf(self, daemon, source=None, param=None):
594b1259
MW
1054 # print "Daemons before:", self.daemons
1055 if daemon in self.daemons.keys():
1056 self.daemons[daemon] = 1
8dd5077d
PG
1057 if param is not None:
1058 self.daemons_options[daemon] = param
594b1259 1059 if source is None:
787e7624 1060 self.cmd("touch /etc/%s/%s.conf" % (self.routertype, daemon))
594b1259
MW
1061 self.waitOutput()
1062 else:
787e7624 1063 self.cmd("cp %s /etc/%s/%s.conf" % (source, self.routertype, daemon))
594b1259 1064 self.waitOutput()
787e7624 1065 self.cmd("chmod 640 /etc/%s/%s.conf" % (self.routertype, daemon))
594b1259 1066 self.waitOutput()
787e7624 1067 self.cmd(
1068 "chown %s:%s /etc/%s/%s.conf"
1069 % (self.routertype, self.routertype, self.routertype, daemon)
1070 )
594b1259 1071 self.waitOutput()
787e7624 1072 if (daemon == "zebra") and (self.daemons["staticd"] == 0):
a2a1134c 1073 # Add staticd with zebra - if it exists
787e7624 1074 staticd_path = os.path.join(self.daemondir, "staticd")
a2a1134c 1075 if os.path.isfile(staticd_path):
787e7624 1076 self.daemons["staticd"] = 1
1077 self.daemons_options["staticd"] = ""
2c805e6c 1078 # Auto-Started staticd has no config, so it will read from zebra config
594b1259 1079 else:
787e7624 1080 logger.info("No daemon {} known".format(daemon))
594b1259 1081 # print "Daemons after:", self.daemons
e1dfa45e 1082
9711fc7e 1083 def startRouter(self, tgen=None):
594b1259 1084 # Disable integrated-vtysh-config
787e7624 1085 self.cmd(
1086 'echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf'
1087 % self.routertype
1088 )
1089 self.cmd(
1090 "chown %s:%svty /etc/%s/vtysh.conf"
1091 % (self.routertype, self.routertype, self.routertype)
1092 )
13e1fc49 1093 # TODO remove the following lines after all tests are migrated to Topogen.
594b1259 1094 # Try to find relevant old logfiles in /tmp and delete them
787e7624 1095 map(os.remove, glob.glob("{}/{}/*.log".format(self.logdir, self.name)))
594b1259 1096 # Remove old core files
787e7624 1097 map(os.remove, glob.glob("{}/{}/*.dmp".format(self.logdir, self.name)))
594b1259
MW
1098 # Remove IP addresses from OS first - we have them in zebra.conf
1099 self.removeIPs()
1100 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
1101 # No error - but return message and skip all the tests
787e7624 1102 if self.daemons["ldpd"] == 1:
1103 ldpd_path = os.path.join(self.daemondir, "ldpd")
2ab85530 1104 if not os.path.isfile(ldpd_path):
222ea88b 1105 logger.info("LDP Test, but no ldpd compiled or installed")
594b1259 1106 return "LDP Test, but no ldpd compiled or installed"
dd4eca4d 1107
787e7624 1108 if version_cmp(platform.release(), "4.5") < 0:
222ea88b 1109 logger.info("LDP Test need Linux Kernel 4.5 minimum")
45619ee3 1110 return "LDP Test need Linux Kernel 4.5 minimum"
9711fc7e
LB
1111 # Check if have mpls
1112 if tgen != None:
1113 self.hasmpls = tgen.hasmpls
1114 if self.hasmpls != True:
787e7624 1115 logger.info(
1116 "LDP/MPLS Tests will be skipped, platform missing module(s)"
1117 )
9711fc7e
LB
1118 else:
1119 # Test for MPLS Kernel modules available
1120 self.hasmpls = False
787e7624 1121 if not module_present("mpls-router"):
1122 logger.info(
1123 "MPLS tests will not run (missing mpls-router kernel module)"
1124 )
1125 elif not module_present("mpls-iptunnel"):
1126 logger.info(
1127 "MPLS tests will not run (missing mpls-iptunnel kernel module)"
1128 )
9711fc7e
LB
1129 else:
1130 self.hasmpls = True
1131 if self.hasmpls != True:
1132 return "LDP/MPLS Tests need mpls kernel modules"
787e7624 1133 self.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels")
44a592b2 1134
787e7624 1135 if self.daemons["eigrpd"] == 1:
1136 eigrpd_path = os.path.join(self.daemondir, "eigrpd")
44a592b2 1137 if not os.path.isfile(eigrpd_path):
222ea88b 1138 logger.info("EIGRP Test, but no eigrpd compiled or installed")
44a592b2
MW
1139 return "EIGRP Test, but no eigrpd compiled or installed"
1140
787e7624 1141 if self.daemons["bfdd"] == 1:
1142 bfdd_path = os.path.join(self.daemondir, "bfdd")
4d45d6d3
RZ
1143 if not os.path.isfile(bfdd_path):
1144 logger.info("BFD Test, but no bfdd compiled or installed")
1145 return "BFD Test, but no bfdd compiled or installed"
1146
99561211
MW
1147 self.restartRouter()
1148 return ""
e1dfa45e 1149
99561211 1150 def restartRouter(self):
e1dfa45e
LB
1151 # Starts actual daemons without init (ie restart)
1152 # cd to per node directory
787e7624 1153 self.cmd("cd {}/{}".format(self.logdir, self.name))
1154 self.cmd("umask 000")
1155 # Re-enable to allow for report per run
2a59a86b 1156 self.reportCores = True
fb80b81b 1157 if self.version == None:
787e7624 1158 self.version = self.cmd(
1159 os.path.join(self.daemondir, "bgpd") + " -v"
1160 ).split()[2]
1161 logger.info("{}: running version: {}".format(self.name, self.version))
594b1259 1162 # Start Zebra first
787e7624 1163 if self.daemons["zebra"] == 1:
1164 zebra_path = os.path.join(self.daemondir, "zebra")
1165 zebra_option = self.daemons_options["zebra"]
1166 self.cmd(
1167 "{0} {1} > zebra.out 2> zebra.err &".format(
1168 zebra_path, zebra_option, self.logdir, self.name
1169 )
1170 )
594b1259 1171 self.waitOutput()
787e7624 1172 logger.debug("{}: {} zebra started".format(self, self.routertype))
1173 sleep(1, "{}: waiting for zebra to start".format(self.name))
a2a1134c 1174 # Start staticd next if required
787e7624 1175 if self.daemons["staticd"] == 1:
1176 staticd_path = os.path.join(self.daemondir, "staticd")
1177 staticd_option = self.daemons_options["staticd"]
1178 self.cmd(
1179 "{0} {1} > staticd.out 2> staticd.err &".format(
1180 staticd_path, staticd_option, self.logdir, self.name
1181 )
1182 )
a2a1134c 1183 self.waitOutput()
787e7624 1184 logger.debug("{}: {} staticd started".format(self, self.routertype))
1185 # Fix Link-Local Addresses
594b1259 1186 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
787e7624 1187 self.cmd(
1188 "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"
1189 )
594b1259
MW
1190 # Now start all the other daemons
1191 for daemon in self.daemons:
2ab85530 1192 # Skip disabled daemons and zebra
787e7624 1193 if self.daemons[daemon] == 0 or daemon == "zebra" or daemon == "staticd":
2ab85530 1194 continue
2ab85530 1195 daemon_path = os.path.join(self.daemondir, daemon)
787e7624 1196 self.cmd(
1197 "{0} {1} > {2}.out 2> {2}.err &".format(
1198 daemon_path, self.daemons_options.get(daemon, ""), daemon
1199 )
1200 )
2ab85530 1201 self.waitOutput()
787e7624 1202 logger.debug("{}: {} {} started".format(self, self.routertype, daemon))
1203
99561211 1204 def getStdErr(self, daemon):
787e7624 1205 return self.getLog("err", daemon)
1206
99561211 1207 def getStdOut(self, daemon):
787e7624 1208 return self.getLog("out", daemon)
1209
99561211 1210 def getLog(self, log, daemon):
787e7624 1211 return self.cmd("cat {}/{}/{}.{}".format(self.logdir, self.name, daemon, log))
f76774ec 1212
c65a7e26
KK
1213 def startRouterDaemons(self, daemons):
1214 # Starts actual daemons without init (ie restart)
1215 # cd to per node directory
1216 self.cmd('cd {}/{}'.format(self.logdir, self.name))
1217 self.cmd('umask 000')
1218 #Re-enable to allow for report per run
1219 self.reportCores = True
1220 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
1221
1222 for daemon in daemons:
1223 if daemon == 'zebra':
1224 # Start Zebra first
1225 if self.daemons['zebra'] == 1:
1226 zebra_path = os.path.join(self.daemondir, 'zebra')
1227 zebra_option = self.daemons_options['zebra']
1228 self.cmd('{0} {1} > zebra.out 2> zebra.err &'.format(
1229 zebra_path, zebra_option, self.logdir, self.name
1230 ))
1231 self.waitOutput()
1232 logger.debug('{}: {} zebra started'.format(self, self.routertype))
1233 sleep(1, '{}: waiting for zebra to start'.format(self.name))
1234
1235 # Fix Link-Local Addresses
1236 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local
1237 # addresses on start. Fix this
1238 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')
1239
1240 if daemon == 'staticd':
1241 # Start staticd next if required
1242 if self.daemons['staticd'] == 1:
1243 staticd_path = os.path.join(self.daemondir, 'staticd')
1244 staticd_option = self.daemons_options['staticd']
1245 self.cmd('{0} {1} > staticd.out 2> staticd.err &'.format(
1246 staticd_path, staticd_option, self.logdir, self.name
1247 ))
1248 self.waitOutput()
1249 logger.debug('{}: {} staticd started'.format(self, self.routertype))
1250 sleep(1, '{}: waiting for staticd to start'.format(self.name))
1251
1252 # Now start all the daemons
1253 # Skip disabled daemons and zebra
1254 if self.daemons[daemon] == 0 or daemon == 'zebra' or daemon == 'staticd':
1255 continue
1256 daemon_path = os.path.join(self.daemondir, daemon)
1257 self.cmd('{0} > {1}.out 2> {1}.err &'.format(
1258 daemon_path, daemon
1259 ))
1260 self.waitOutput()
1261 logger.debug('{}: {} {} started'.format(self, self.routertype, daemon))
1262 sleep(1, '{}: waiting for {} to start'.format(self.name, daemon))
1263
1264 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
1265
1266 if re.search(r"No such file or directory", rundaemons):
1267 return "Daemons are not running"
1268
1269 return ""
1270
1271 def killRouterDaemons(self, daemons, wait=True, assertOnError=True,
1272 minErrorVersion='5.1'):
1273 # Kill Running Quagga or FRR specific
1274 # Daemons(user specified daemon only) using SIGKILL
1275 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
1276 errors = ""
1277 daemonsNotRunning = []
1278 if re.search(r"No such file or directory", rundaemons):
1279 return errors
1280 for daemon in daemons:
1281 if rundaemons is not None and daemon in rundaemons:
1282 numRunning = 0
1283 for d in StringIO.StringIO(rundaemons):
1284 if re.search(r"%s" % daemon, d):
1285 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
1286 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
1287 logger.info('{}: killing {}'.format(
1288 self.name,
1289 os.path.basename(d.rstrip().rsplit(".", 1)[0])
1290 ))
1291 self.cmd('kill -9 %s' % daemonpid)
1292 self.waitOutput()
1293 if pid_exists(int(daemonpid)):
1294 numRunning += 1
1295 if wait and numRunning > 0:
1296 sleep(2, '{}: waiting for {} daemon to be stopped'.\
1297 format(self.name, daemon))
1298 # 2nd round of kill if daemons didn't exit
1299 for d in StringIO.StringIO(rundaemons):
1300 if re.search(r"%s" % daemon, d):
1301 daemonpid = \
1302 self.cmd('cat %s' % d.rstrip()).rstrip()
1303 if (daemonpid.isdigit() and pid_exists(
1304 int(daemonpid))):
1305 logger.info('{}: killing {}'.format(
1306 self.name,
1307 os.path.basename(d.rstrip().\
1308 rsplit(".", 1)[0])
1309 ))
1310 self.cmd('kill -9 %s' % daemonpid)
1311 self.waitOutput()
1312 self.cmd('rm -- {}'.format(d.rstrip()))
1313 if wait:
1314 errors = self.checkRouterCores(reportOnce=True)
1315 if self.checkRouterVersion('<', minErrorVersion):
1316 #ignore errors in old versions
1317 errors = ""
1318 if assertOnError and len(errors) > 0:
1319 assert "Errors found - details follow:" == 0, errors
c65a7e26
KK
1320 else:
1321 daemonsNotRunning.append(daemon)
1322 if len(daemonsNotRunning) > 0:
1323 errors = errors+"Daemons are not running", daemonsNotRunning
1324
1325 return errors
1326
2a59a86b
LB
1327 def checkRouterCores(self, reportLeaks=True, reportOnce=False):
1328 if reportOnce and not self.reportCores:
1329 return
1330 reportMade = False
83c26937 1331 traces = ""
f76774ec 1332 for daemon in self.daemons:
787e7624 1333 if self.daemons[daemon] == 1:
f76774ec 1334 # Look for core file
787e7624 1335 corefiles = glob.glob(
1336 "{}/{}/{}_core*.dmp".format(self.logdir, self.name, daemon)
1337 )
1338 if len(corefiles) > 0:
f76774ec 1339 daemon_path = os.path.join(self.daemondir, daemon)
787e7624 1340 backtrace = subprocess.check_output(
1341 [
1342 "gdb {} {} --batch -ex bt 2> /dev/null".format(
1343 daemon_path, corefiles[0]
1344 )
1345 ],
1346 shell=True,
1347 )
1348 sys.stderr.write(
1349 "\n%s: %s crashed. Core file found - Backtrace follows:\n"
1350 % (self.name, daemon)
1351 )
f76774ec 1352 sys.stderr.write("%s" % backtrace)
787e7624 1353 traces = (
1354 traces
1355 + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s"
1356 % (self.name, daemon, backtrace)
1357 )
2a59a86b 1358 reportMade = True
f76774ec
LB
1359 elif reportLeaks:
1360 log = self.getStdErr(daemon)
1361 if "memstats" in log:
787e7624 1362 sys.stderr.write(
1363 "%s: %s has memory leaks:\n" % (self.name, daemon)
1364 )
1365 traces = traces + "\n%s: %s has memory leaks:\n" % (
1366 self.name,
1367 daemon,
1368 )
f76774ec 1369 log = re.sub("core_handler: ", "", log)
787e7624 1370 log = re.sub(
1371 r"(showing active allocations in memory group [a-zA-Z0-9]+)",
1372 r"\n ## \1",
1373 log,
1374 )
f76774ec
LB
1375 log = re.sub("memstats: ", " ", log)
1376 sys.stderr.write(log)
2a59a86b 1377 reportMade = True
f76774ec 1378 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
787e7624 1379 if checkAddressSanitizerError(
1380 self.getStdErr(daemon), self.name, daemon
1381 ):
1382 sys.stderr.write(
1383 "%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon)
1384 )
1385 traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % (
1386 self.name,
1387 daemon,
1388 )
2a59a86b
LB
1389 reportMade = True
1390 if reportMade:
1391 self.reportCores = False
83c26937 1392 return traces
f76774ec 1393
594b1259 1394 def checkRouterRunning(self):
597cabb7
MW
1395 "Check if router daemons are running and collect crashinfo they don't run"
1396
594b1259
MW
1397 global fatal_error
1398
787e7624 1399 daemonsRunning = self.cmd(
1400 'vtysh -c "show logging" | grep "Logging configuration for"'
1401 )
4942f298
MW
1402 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
1403 if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"):
1404 return "%s: vtysh killed by AddressSanitizer" % (self.name)
1405
594b1259
MW
1406 for daemon in self.daemons:
1407 if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
1408 sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
d2132114 1409 if daemon is "staticd":
787e7624 1410 sys.stderr.write(
1411 "You may have a copy of staticd installed but are attempting to test against\n"
1412 )
1413 sys.stderr.write(
1414 "a version of FRR that does not have staticd, please cleanup the install dir\n"
1415 )
d2132114 1416
594b1259 1417 # Look for core file
787e7624 1418 corefiles = glob.glob(
1419 "{}/{}/{}_core*.dmp".format(self.logdir, self.name, daemon)
1420 )
1421 if len(corefiles) > 0:
2ab85530 1422 daemon_path = os.path.join(self.daemondir, daemon)
787e7624 1423 backtrace = subprocess.check_output(
1424 [
1425 "gdb {} {} --batch -ex bt 2> /dev/null".format(
1426 daemon_path, corefiles[0]
1427 )
1428 ],
1429 shell=True,
1430 )
1431 sys.stderr.write(
1432 "\n%s: %s crashed. Core file found - Backtrace follows:\n"
1433 % (self.name, daemon)
1434 )
594b1259
MW
1435 sys.stderr.write("%s\n" % backtrace)
1436 else:
1437 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
787e7624 1438 if os.path.isfile(
1439 "{}/{}/{}.log".format(self.logdir, self.name, daemon)
1440 ):
1441 log_tail = subprocess.check_output(
1442 [
1443 "tail -n20 {}/{}/{}.log 2> /dev/null".format(
1444 self.logdir, self.name, daemon
1445 )
1446 ],
1447 shell=True,
1448 )
1449 sys.stderr.write(
1450 "\nFrom %s %s %s log file:\n"
1451 % (self.routertype, self.name, daemon)
1452 )
594b1259 1453 sys.stderr.write("%s\n" % log_tail)
4942f298 1454
597cabb7 1455 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
787e7624 1456 if checkAddressSanitizerError(
1457 self.getStdErr(daemon), self.name, daemon
1458 ):
1459 return "%s: Daemon %s not running - killed by AddressSanitizer" % (
1460 self.name,
1461 daemon,
1462 )
84379e8e 1463
594b1259
MW
1464 return "%s: Daemon %s not running" % (self.name, daemon)
1465 return ""
fb80b81b
LB
1466
1467 def checkRouterVersion(self, cmpop, version):
1468 """
1469 Compares router version using operation `cmpop` with `version`.
1470 Valid `cmpop` values:
1471 * `>=`: has the same version or greater
1472 * '>': has greater version
1473 * '=': has the same version
1474 * '<': has a lesser version
1475 * '<=': has the same version or lesser
1476
1477 Usage example: router.checkRouterVersion('>', '1.0')
1478 """
6bfe4b8b
MW
1479
1480 # Make sure we have version information first
1481 if self.version == None:
787e7624 1482 self.version = self.cmd(
1483 os.path.join(self.daemondir, "bgpd") + " -v"
1484 ).split()[2]
1485 logger.info("{}: running version: {}".format(self.name, self.version))
6bfe4b8b 1486
fb80b81b
LB
1487 rversion = self.version
1488 if rversion is None:
1489 return False
1490
1491 result = version_cmp(rversion, version)
787e7624 1492 if cmpop == ">=":
fb80b81b 1493 return result >= 0
787e7624 1494 if cmpop == ">":
fb80b81b 1495 return result > 0
787e7624 1496 if cmpop == "=":
fb80b81b 1497 return result == 0
787e7624 1498 if cmpop == "<":
fb80b81b 1499 return result < 0
787e7624 1500 if cmpop == "<":
fb80b81b 1501 return result < 0
787e7624 1502 if cmpop == "<=":
fb80b81b
LB
1503 return result <= 0
1504
594b1259
MW
1505 def get_ipv6_linklocal(self):
1506 "Get LinkLocal Addresses from interfaces"
1507
1508 linklocal = []
1509
787e7624 1510 ifaces = self.cmd("ip -6 address")
594b1259 1511 # Fix newlines (make them all the same)
787e7624 1512 ifaces = ("\n".join(ifaces.splitlines()) + "\n").splitlines()
1513 interface = ""
1514 ll_per_if_count = 0
594b1259 1515 for line in ifaces:
787e7624 1516 m = re.search("[0-9]+: ([^:@]+)[@if0-9:]+ <", line)
594b1259
MW
1517 if m:
1518 interface = m.group(1)
1519 ll_per_if_count = 0
787e7624 1520 m = re.search(
1521 "inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link",
1522 line,
1523 )
594b1259
MW
1524 if m:
1525 local = m.group(1)
1526 ll_per_if_count += 1
787e7624 1527 if ll_per_if_count > 1:
594b1259
MW
1528 linklocal += [["%s-%s" % (interface, ll_per_if_count), local]]
1529 else:
1530 linklocal += [[interface, local]]
1531 return linklocal
787e7624 1532
80eeefb7
MW
1533 def daemon_available(self, daemon):
1534 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
1535
2ab85530
RZ
1536 daemon_path = os.path.join(self.daemondir, daemon)
1537 if not os.path.isfile(daemon_path):
80eeefb7 1538 return False
787e7624 1539 if daemon == "ldpd":
1540 if version_cmp(platform.release(), "4.5") < 0:
b431b554 1541 return False
787e7624 1542 if not module_present("mpls-router", load=False):
80eeefb7 1543 return False
787e7624 1544 if not module_present("mpls-iptunnel", load=False):
b431b554 1545 return False
80eeefb7 1546 return True
f2d6ce41 1547
80eeefb7
MW
1548 def get_routertype(self):
1549 "Return the type of Router (frr or quagga)"
1550
1551 return self.routertype
787e7624 1552
50c40bde
MW
1553 def report_memory_leaks(self, filename_prefix, testscript):
1554 "Report Memory Leaks to file prefixed with given string"
1555
1556 leakfound = False
1557 filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt"
1558 for daemon in self.daemons:
787e7624 1559 if self.daemons[daemon] == 1:
50c40bde
MW
1560 log = self.getStdErr(daemon)
1561 if "memstats" in log:
1562 # Found memory leak
787e7624 1563 logger.info(
1564 "\nRouter {} {} StdErr Log:\n{}".format(self.name, daemon, log)
1565 )
50c40bde
MW
1566 if not leakfound:
1567 leakfound = True
1568 # Check if file already exists
1569 fileexists = os.path.isfile(filename)
1570 leakfile = open(filename, "a")
1571 if not fileexists:
1572 # New file - add header
787e7624 1573 leakfile.write(
1574 "# Memory Leak Detection for topotest %s\n\n"
1575 % testscript
1576 )
50c40bde
MW
1577 leakfile.write("## Router %s\n" % self.name)
1578 leakfile.write("### Process %s\n" % daemon)
1579 log = re.sub("core_handler: ", "", log)
787e7624 1580 log = re.sub(
1581 r"(showing active allocations in memory group [a-zA-Z0-9]+)",
1582 r"\n#### \1\n",
1583 log,
1584 )
50c40bde
MW
1585 log = re.sub("memstats: ", " ", log)
1586 leakfile.write(log)
1587 leakfile.write("\n")
1588 if leakfound:
1589 leakfile.close()
80eeefb7 1590
787e7624 1591
7cc96035 1592class LinuxRouter(Router):
4b1d6d4d 1593 "A Linux Router Node with IPv4/IPv6 forwarding enabled."
7cc96035
DS
1594
1595 def __init__(self, name, **params):
1596 Router.__init__(self, name, **params)
1597
1598 def config(self, **params):
1599 Router.config(self, **params)
1600 # Enable forwarding on the router
787e7624 1601 assert_sysctl(self, "net.ipv4.ip_forward", 1)
1602 assert_sysctl(self, "net.ipv6.conf.all.forwarding", 1)
d29fb5bd 1603 # Enable coredumps
787e7624 1604 assert_sysctl(self, "kernel.core_uses_pid", 1)
1605 assert_sysctl(self, "fs.suid_dumpable", 1)
1606 # this applies to the kernel not the namespace...
1607 # original on ubuntu 17.x, but apport won't save as in namespace
d29fb5bd 1608 # |/usr/share/apport/apport %p %s %c %d %P
787e7624 1609 corefile = "%e_core-sig_%s-pid_%p.dmp"
1610 assert_sysctl(self, "kernel.core_pattern", corefile)
d29fb5bd 1611
7cc96035
DS
1612 def terminate(self):
1613 """
1614 Terminate generic LinuxRouter Mininet instance
1615 """
787e7624 1616 set_sysctl(self, "net.ipv4.ip_forward", 0)
1617 set_sysctl(self, "net.ipv6.conf.all.forwarding", 0)
7cc96035 1618 Router.terminate(self)
594b1259 1619
787e7624 1620
4b1d6d4d
DS
1621class FreeBSDRouter(Router):
1622 "A FreeBSD Router Node with IPv4/IPv6 forwarding enabled."
1623
1624 def __init__(eslf, name, **params):
1625 Router.__init__(Self, name, **params)
1626
1627
594b1259
MW
1628class LegacySwitch(OVSSwitch):
1629 "A Legacy Switch without OpenFlow"
1630
1631 def __init__(self, name, **params):
787e7624 1632 OVSSwitch.__init__(self, name, failMode="standalone", **params)
594b1259 1633 self.switchIP = None