]>
Commit | Line | Data |
---|---|---|
594b1259 MW |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # topotest.py | |
5 | # Library of helper functions for NetDEF Topology Tests | |
6 | # | |
7 | # Copyright (c) 2016 by | |
8 | # Network Device Education Foundation, Inc. ("NetDEF") | |
9 | # | |
10 | # Permission to use, copy, modify, and/or distribute this software | |
11 | # for any purpose with or without fee is hereby granted, provided | |
12 | # that the above copyright notice and this permission notice appear | |
13 | # in all copies. | |
14 | # | |
15 | # THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES | |
16 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
17 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR | |
18 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY | |
19 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | |
20 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | |
21 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE | |
22 | # OF THIS SOFTWARE. | |
23 | # | |
24 | ||
7bd28cfc | 25 | import json |
594b1259 | 26 | import os |
50c40bde | 27 | import errno |
594b1259 MW |
28 | import re |
29 | import sys | |
fd858290 | 30 | import functools |
594b1259 MW |
31 | import glob |
32 | import StringIO | |
33 | import subprocess | |
1fca63c1 | 34 | import tempfile |
594b1259 | 35 | import platform |
17070436 | 36 | import difflib |
570f25d8 | 37 | import time |
594b1259 | 38 | |
6c131bd3 | 39 | from lib.topolog import logger |
849224d4 | 40 | from copy import deepcopy |
6c131bd3 | 41 | |
04ce2b97 RZ |
42 | if sys.version_info[0] > 2: |
43 | import configparser | |
44 | else: | |
45 | import ConfigParser as configparser | |
46 | ||
594b1259 MW |
47 | from mininet.topo import Topo |
48 | from mininet.net import Mininet | |
49 | from mininet.node import Node, OVSSwitch, Host | |
50 | from mininet.log import setLogLevel, info | |
51 | from mininet.cli import CLI | |
52 | from mininet.link import Intf | |
53 | ||
787e7624 | 54 | |
3668ed8d RZ |
55 | class 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 | 80 | def 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 | |
246 | def 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 |
278 | def 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 | 290 | def 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 |
298 | def 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 |
348 | def 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 |
404 | def 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 |
419 | def 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 | 441 | def 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 | |
452 | def 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 |
459 | def 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 |
470 | def 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 | 485 | def 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 |
501 | def module_present_freebsd(module, load): |
502 | return True | |
503 | ||
787e7624 | 504 | |
cc95fbd9 DS |
505 | def 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 |
512 | def 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 |
568 | def 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 |
584 | def 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 |
604 | def 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 |
631 | def 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 |
649 | def 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 |
690 | def 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 |
730 | def 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 |
781 | def 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 |
793 | def 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 | 832 | def 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 |
848 | def 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 |
865 | def 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 | |
870 | class 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 | 1592 | class 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 |
1621 | class 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 |
1628 | class 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 |