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