5 # Library of helper functions for NetDEF Topology Tests
7 # Copyright (c) 2016 by
8 # Network Device Education Foundation, Inc. ("NetDEF")
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
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
39 from lib
.topolog
import logger
40 from copy
import deepcopy
42 if sys
.version_info
[0] > 2:
45 import ConfigParser
as configparser
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
55 def gdb_core(obj
, daemon
, corefiles
):
71 gdbcmds
= [["-ex", i
.strip()] for i
in gdbcmds
.strip().split("\n")]
72 gdbcmds
= [item
for sl
in gdbcmds
for item
in sl
]
74 daemon_path
= os
.path
.join(obj
.daemondir
, daemon
)
75 backtrace
= subprocess
.check_output(
76 ["gdb", daemon_path
, corefiles
[0], "--batch"] + gdbcmds
79 "\n%s: %s crashed. Core file found - Backtrace follows:\n" % (obj
.name
, daemon
)
81 sys
.stderr
.write("%s" % backtrace
)
85 class json_cmp_result(object):
86 "json_cmp result class for better assertion messages"
91 def add_error(self
, error
):
92 "Append error message to the result"
93 for line
in error
.splitlines():
94 self
.errors
.append(line
)
97 "Returns True if there were errors, otherwise False."
98 return len(self
.errors
) > 0
100 def gen_report(self
):
101 headline
= ["Generated JSON diff error report:", ""]
102 return headline
+ self
.errors
106 "Generated JSON diff error report:\n\n\n" + "\n".join(self
.errors
) + "\n\n"
110 def gen_json_diff_report(d1
, d2
, exact
=False, path
="> $", acc
=(0, "")):
112 Internal workhorse which compares two JSON data structures and generates an error report suited to be read by a human eye.
116 if isinstance(v
, (dict, list)):
117 return "\t" + "\t".join(
118 json
.dumps(v
, indent
=4, separators
=(",", ": ")).splitlines(True)
121 return "'{}'".format(v
)
124 if isinstance(v
, (list, tuple)):
126 elif isinstance(v
, dict):
128 elif isinstance(v
, (int, float)):
130 elif isinstance(v
, bool):
132 elif isinstance(v
, str):
137 def get_errors(other_acc
):
140 def get_errors_n(other_acc
):
143 def add_error(acc
, msg
, points
=1):
144 return (acc
[0] + points
, acc
[1] + "{}: {}\n".format(path
, msg
))
146 def merge_errors(acc
, other_acc
):
147 return (acc
[0] + other_acc
[0], acc
[1] + other_acc
[1])
150 return "{}[{}]".format(path
, idx
)
153 return "{}->{}".format(path
, key
)
155 def has_errors(other_acc
):
156 return other_acc
[0] > 0
159 not isinstance(d1
, (list, dict))
160 and not isinstance(d2
, (list, dict))
165 not isinstance(d1
, (list, dict))
166 and not isinstance(d2
, (list, dict))
171 "d1 has element with value '{}' but in d2 it has value '{}'".format(d1
, d2
),
175 and isinstance(d2
, list)
176 and ((len(d2
) > 0 and d2
[0] == "__ordered__") or exact
)
180 if len(d1
) != len(d2
):
183 "d1 has Array of length {} but in d2 it is of length {}".format(
188 for idx
, v1
, v2
in zip(range(0, len(d1
)), d1
, d2
):
190 acc
, gen_json_diff_report(v1
, v2
, exact
=exact
, path
=add_idx(idx
))
192 elif isinstance(d1
, list) and isinstance(d2
, list):
193 if len(d1
) < len(d2
):
196 "d1 has Array of length {} but in d2 it is of length {}".format(
201 for idx2
, v2
in zip(range(0, len(d2
)), d2
):
205 for idx1
, v1
in zip(range(0, len(d1
)), d1
):
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
))
209 if not has_errors(tmp_diff
):
213 elif not closest_diff
or get_errors_n(tmp_diff
) < get_errors_n(
216 closest_diff
= tmp_diff
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))
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
),
229 if not found_match
and not isinstance(v2
, (list, dict)):
232 "d2 has the following element at index {} which is not present in d1: {}".format(
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
:
246 acc
, gen_json_diff_report(d1
[k
], d2
[k
], exact
=exact
, path
=add_key(k
))
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
:
253 acc
, "d1 has key '{}' which is not supposed to be present".format(k
)
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
:
262 acc
, gen_json_diff_report(d1
[k
], d2
[k
], exact
=exact
, path
=add_key(k
))
267 "d1 has element of type '{}' but the corresponding element in d2 is of type '{}'".format(
268 json_type(d1
), json_type(d2
)
276 def json_cmp(d1
, d2
, exact
=False):
278 JSON compare function. Receives two parameters:
279 * `d1`: parsed JSON data structure
280 * `d2`: parsed JSON data structure
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
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
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
298 (errors_n
, errors
) = gen_json_diff_report(deepcopy(d1
), deepcopy(d2
), exact
=exact
)
301 result
= json_cmp_result()
302 result
.add_error(errors
)
308 def router_output_cmp(router
, cmd
, expected
):
310 Runs `cmd` in router and compares the output with `expected`.
313 normalize_text(router
.vtysh_cmd(cmd
)),
314 normalize_text(expected
),
315 title1
="Current output",
316 title2
="Expected output",
320 def router_json_cmp(router
, cmd
, data
, exact
=False):
322 Runs `cmd` that returns JSON data (normally the command ends with 'json')
323 and compare with `data` contents.
325 return json_cmp(router
.vtysh_cmd(cmd
, isjson
=True), data
, exact
)
328 def run_and_expect(func
, what
, count
=20, wait
=3):
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.
334 Returns (True, func-return) on success or
335 (False, func-return) on failure.
339 Helper functions to use with this function:
343 start_time
= time
.time()
344 func_name
= "<unknown>"
345 if func
.__class
__ == functools
.partial
:
346 func_name
= func
.func
.__name
__
348 func_name
= func
.__name
__
351 "'{}' polling started (interval {} secs, maximum wait {} secs)".format(
352 func_name
, wait
, int(wait
* count
)
363 end_time
= time
.time()
365 "'{}' succeeded after {:.2f} seconds".format(
366 func_name
, end_time
- start_time
369 return (True, result
)
371 end_time
= time
.time()
373 "'{}' failed after {:.2f} seconds".format(func_name
, end_time
- start_time
)
375 return (False, result
)
378 def run_and_expect_type(func
, etype
, count
=20, wait
=3, avalue
=None):
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.
384 This function is used when you want to test the return type and,
385 optionally, the return value.
387 Returns (True, func-return) on success or
388 (False, func-return) on failure.
390 start_time
= time
.time()
391 func_name
= "<unknown>"
392 if func
.__class
__ == functools
.partial
:
393 func_name
= func
.func
.__name
__
395 func_name
= func
.__name
__
398 "'{}' polling started (interval {} secs, maximum wait {} secs)".format(
399 func_name
, wait
, int(wait
* count
)
405 if not isinstance(result
, etype
):
407 "Expected result type '{}' got '{}' instead".format(etype
, type(result
))
413 if etype
!= type(None) and avalue
!= None and result
!= avalue
:
414 logger
.debug("Expected value '{}' got '{}' instead".format(avalue
, result
))
419 end_time
= time
.time()
421 "'{}' succeeded after {:.2f} seconds".format(
422 func_name
, end_time
- start_time
425 return (True, result
)
427 end_time
= time
.time()
429 "'{}' failed after {:.2f} seconds".format(func_name
, end_time
- start_time
)
431 return (False, result
)
435 "Converting Integer to DPID"
439 dpid
= "0" * (16 - len(dpid
)) + dpid
443 "Unable to derive default datapath ID - "
444 "please either specify a dpid or use a "
445 "canonical switch name such as s23."
450 "Check whether pid exists in the current process table."
455 os
.waitpid(pid
, os
.WNOHANG
)
460 except OSError as err
:
461 if err
.errno
== errno
.ESRCH
:
462 # ESRCH == No such process
464 elif err
.errno
== errno
.EPERM
:
465 # EPERM clearly means there's a process to deny access to
468 # According to "man 2 kill" possible error values are
469 # (EINVAL, EPERM, ESRCH)
475 def get_textdiff(text1
, text2
, title1
="", title2
="", **opts
):
476 "Returns empty string if same or formatted diff"
479 difflib
.unified_diff(text1
, text2
, fromfile
=title1
, tofile
=title2
, **opts
)
481 # Clean up line endings
482 diff
= os
.linesep
.join([s
for s
in diff
.splitlines() if s
])
486 def difflines(text1
, text2
, title1
="", title2
="", **opts
):
487 "Wrapper for get_textdiff to avoid string transformations."
488 text1
= ("\n".join(text1
.rstrip().splitlines()) + "\n").splitlines(1)
489 text2
= ("\n".join(text2
.rstrip().splitlines()) + "\n").splitlines(1)
490 return get_textdiff(text1
, text2
, title1
, title2
, **opts
)
493 def get_file(content
):
495 Generates a temporary file in '/tmp' with `content` and returns the file name.
497 fde
= tempfile
.NamedTemporaryFile(mode
="w", delete
=False)
504 def normalize_text(text
):
506 Strips formating spaces/tabs, carriage returns and trailing whitespace.
508 text
= re
.sub(r
"[ \t]+", " ", text
)
509 text
= re
.sub(r
"\r", "", text
)
511 # Remove whitespace in the middle of text.
512 text
= re
.sub(r
"[ \t]+\n", "\n", text
)
513 # Remove whitespace at the end of the text.
519 def module_present_linux(module
, load
):
521 Returns whether `module` is present.
523 If `load` is true, it will try to load it via modprobe.
525 with
open("/proc/modules", "r") as modules_file
:
526 if module
.replace("-", "_") in modules_file
.read():
528 cmd
= "/sbin/modprobe {}{}".format("" if load
else "-n ", module
)
529 if os
.system(cmd
) != 0:
535 def module_present_freebsd(module
, load
):
539 def module_present(module
, load
=True):
540 if sys
.platform
.startswith("linux"):
541 return module_present_linux(module
, load
)
542 elif sys
.platform
.startswith("freebsd"):
543 return module_present_freebsd(module
, load
)
546 def version_cmp(v1
, v2
):
548 Compare two version strings and returns:
550 * `-1`: if `v1` is less than `v2`
551 * `0`: if `v1` is equal to `v2`
552 * `1`: if `v1` is greater than `v2`
554 Raises `ValueError` if versions are not well formated.
556 vregex
= r
"(?P<whole>\d+(\.(\d+))*)"
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")
563 v1g
= v1m
.group("whole").split(".")
564 v2g
= v2m
.group("whole").split(".")
566 # Get the longest version string
571 # Reverse list because we are going to pop the tail
574 for _
in range(vnum
):
602 def interface_set_status(node
, ifacename
, ifaceaction
=False, vrf_name
=None):
604 str_ifaceaction
= "no shutdown"
606 str_ifaceaction
= "shutdown"
608 cmd
= 'vtysh -c "configure terminal" -c "interface {0}" -c "{1}"'.format(
609 ifacename
, str_ifaceaction
612 cmd
= 'vtysh -c "configure terminal" -c "interface {0} vrf {1}" -c "{2}"'.format(
613 ifacename
, vrf_name
, str_ifaceaction
618 def ip4_route_zebra(node
, vrf_name
=None):
620 Gets an output of 'show ip route' command. It can be used
621 with comparing the output to a reference
624 tmp
= node
.vtysh_cmd("show ip route")
626 tmp
= node
.vtysh_cmd("show ip route vrf {0}".format(vrf_name
))
627 output
= re
.sub(r
" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp
)
629 lines
= output
.splitlines()
631 while lines
and (not lines
[0].strip() or not header_found
):
632 if "o - offload failure" in lines
[0]:
635 return "\n".join(lines
)
638 def ip6_route_zebra(node
, vrf_name
=None):
640 Retrieves the output of 'show ipv6 route [vrf vrf_name]', then
641 canonicalizes it by eliding link-locals.
645 tmp
= node
.vtysh_cmd("show ipv6 route")
647 tmp
= node
.vtysh_cmd("show ipv6 route vrf {0}".format(vrf_name
))
650 output
= re
.sub(r
" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp
)
652 # Mask out the link-local addresses
653 output
= re
.sub(r
"fe80::[^ ]+,", "fe80::XXXX:XXXX:XXXX:XXXX,", output
)
655 lines
= output
.splitlines()
657 while lines
and (not lines
[0].strip() or not header_found
):
658 if "o - offload failure" in lines
[0]:
662 return "\n".join(lines
)
665 def proto_name_to_number(protocol
):
680 ) # default return same as input
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.
701 output
= normalize_text(node
.run("ip route")).splitlines()
704 columns
= line
.split(" ")
705 route
= result
[columns
[0]] = {}
707 for column
in columns
:
709 route
["dev"] = column
711 route
["via"] = column
713 # translate protocol names back to numbers
714 route
["proto"] = proto_name_to_number(column
)
716 route
["metric"] = column
718 route
["scope"] = column
724 def ip4_vrf_route(node
):
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.
742 output
= normalize_text(
743 node
.run("ip route show vrf {0}-cust1".format(node
.name
))
748 columns
= line
.split(" ")
749 route
= result
[columns
[0]] = {}
751 for column
in columns
:
753 route
["dev"] = column
755 route
["via"] = column
757 # translate protocol names back to numbers
758 route
["proto"] = proto_name_to_number(column
)
760 route
["metric"] = column
762 route
["scope"] = column
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.
785 output
= normalize_text(node
.run("ip -6 route")).splitlines()
788 columns
= line
.split(" ")
789 route
= result
[columns
[0]] = {}
791 for column
in columns
:
793 route
["dev"] = column
795 route
["via"] = column
797 # translate protocol names back to numbers
798 route
["proto"] = proto_name_to_number(column
)
800 route
["metric"] = column
802 route
["pref"] = column
808 def ip6_vrf_route(node
):
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.
825 output
= normalize_text(
826 node
.run("ip -6 route show vrf {0}-cust1".format(node
.name
))
830 columns
= line
.split(" ")
831 route
= result
[columns
[0]] = {}
833 for column
in columns
:
835 route
["dev"] = column
837 route
["via"] = column
839 # translate protocol names back to numbers
840 route
["proto"] = proto_name_to_number(column
)
842 route
["metric"] = column
844 route
["pref"] = column
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.
869 "from": "1.2.0.0/16",
874 output
= normalize_text(node
.run("ip rule")).splitlines()
877 columns
= line
.split(" ")
880 # remove last character, since it is ':'
881 pref
= columns
[0][:-1]
884 for column
in columns
:
886 route
["from"] = column
890 route
["proto"] = column
892 route
["iif"] = column
894 route
["fwmark"] = column
901 def sleep(amount
, reason
=None):
903 Sleep wrapper that registers in the log the amount of sleep
906 logger
.info("Sleeping for {} seconds".format(amount
))
908 logger
.info(reason
+ " ({} seconds)".format(amount
))
913 def checkAddressSanitizerError(output
, router
, component
, logdir
=""):
914 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
916 def processAddressSanitizerError(asanErrorRe
, output
, router
, component
):
918 "%s: %s triggered an exception by AddressSanitizer\n" % (router
, component
)
920 # Sanitizer Error found in log
921 pidMark
= asanErrorRe
.group(1)
922 addressSanitizerLog
= re
.search(
923 "%s(.*)%s" % (pidMark
, pidMark
), output
, re
.DOTALL
925 if addressSanitizerLog
:
926 # Find Calling Test. Could be multiple steps back
927 testframe
=sys
._current
_frames
().values()[0]
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__"])
936 testframe
=testframe
.f_back
938 # somehow couldn't find the test script.
939 callingTest
="unknownTest"
941 # Now finding Calling Procedure
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
959 # something wrong - couldn't found the calling test function
960 callingProc
="unknownProc"
961 with
open("/tmp/AddressSanitzer.txt", "a") as addrSanFile
:
963 "AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n"
964 % (callingTest
, callingProc
, router
)
967 "\n".join(addressSanitizerLog
.group(1).splitlines()) + "\n"
969 addrSanFile
.write("## Error: %s\n\n" % asanErrorRe
.group(2))
971 "### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n"
972 % (callingTest
, callingProc
, router
)
976 + "\n ".join(addressSanitizerLog
.group(1).splitlines())
979 addrSanFile
.write("\n---------------\n")
983 addressSanitizerError
= re
.search(
984 "(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ", output
986 if addressSanitizerError
:
987 processAddressSanitizerError(addressSanitizerError
, output
, router
, component
)
990 # No Address Sanitizer Error in Output. Now check for AddressSanitizer daemon file
992 filepattern
=logdir
+"/"+router
+"/"+component
+".asan.*"
993 logger
.debug("Log check for %s on %s, pattern %s\n" % (component
, router
, filepattern
))
994 for file in glob
.glob(filepattern
):
995 with
open(file, "r") as asanErrorFile
:
996 asanError
=asanErrorFile
.read()
997 addressSanitizerError
= re
.search(
998 "(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ", asanError
1000 if addressSanitizerError
:
1001 processAddressSanitizerError(addressSanitizerError
, asanError
, router
, component
)
1006 def addRouter(topo
, name
):
1007 "Adding a FRRouter to Topology"
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
)
1020 def set_sysctl(node
, sysctl
, value
):
1021 "Set a sysctl value and return None on success or an error string"
1022 valuestr
= "{}".format(value
)
1023 command
= "sysctl {0}={1}".format(sysctl
, valuestr
)
1024 cmdret
= node
.cmd(command
)
1026 matches
= re
.search(r
"([^ ]+) = ([^\s]+)", cmdret
)
1029 if matches
.group(1) != sysctl
:
1031 if matches
.group(2) != valuestr
:
1037 def 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
1043 "A Node with IPv4/IPv6 forwarding enabled"
1045 def __init__(self
, name
, **params
):
1046 super(Router
, self
).__init
__(name
, **params
)
1047 self
.logdir
= params
.get("logdir")
1049 # Backward compatibility:
1050 # Load configuration defaults like topogen.
1051 self
.config_defaults
= configparser
.ConfigParser(
1053 "verbosity": "info",
1054 "frrdir": "/usr/lib/frr",
1055 "routertype": "frr",
1059 self
.config_defaults
.read(
1060 os
.path
.join(os
.path
.dirname(os
.path
.realpath(__file__
)), "../pytest.ini")
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:
1066 cur_test
= os
.environ
["PYTEST_CURRENT_TEST"]
1067 self
.logdir
= "/tmp/topotests/" + cur_test
[
1068 cur_test
.find("/")+1 : cur_test
.find(".py")
1071 # If the logdir is not created, then create it and set the
1072 # appropriated permissions.
1073 if not os
.path
.isdir(self
.logdir
):
1074 os
.system("mkdir -p " + self
.logdir
+ "/" + name
)
1075 os
.system("chmod -R go+rw /tmp/topotests")
1076 # Erase logs of previous run
1077 os
.system("rm -rf " + self
.logdir
+ "/" + name
)
1079 self
.daemondir
= None
1080 self
.hasmpls
= False
1081 self
.routertype
= "frr"
1101 self
.daemons_options
= {"zebra": ""}
1102 self
.reportCores
= True
1105 def _config_frr(self
, **params
):
1106 "Configure FRR binaries"
1107 self
.daemondir
= params
.get("frrdir")
1108 if self
.daemondir
is None:
1109 self
.daemondir
= self
.config_defaults
.get("topogen", "frrdir")
1111 zebra_path
= os
.path
.join(self
.daemondir
, "zebra")
1112 if not os
.path
.isfile(zebra_path
):
1113 raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path
))
1115 # pylint: disable=W0221
1116 # Some params are only meaningful for the parent class.
1117 def config(self
, **params
):
1118 super(Router
, self
).config(**params
)
1120 # User did not specify the daemons directory, try to autodetect it.
1121 self
.daemondir
= params
.get("daemondir")
1122 if self
.daemondir
is None:
1123 self
.routertype
= params
.get(
1124 "routertype", self
.config_defaults
.get("topogen", "routertype")
1126 self
._config
_frr
(**params
)
1128 # Test the provided path
1129 zpath
= os
.path
.join(self
.daemondir
, "zebra")
1130 if not os
.path
.isfile(zpath
):
1131 raise Exception("No zebra binary found in {}".format(zpath
))
1132 # Allow user to specify routertype when the path was specified.
1133 if params
.get("routertype") is not None:
1134 self
.routertype
= params
.get("routertype")
1136 self
.cmd("ulimit -c unlimited")
1137 # Set ownership of config files
1138 self
.cmd("chown {0}:{0}vty /etc/{0}".format(self
.routertype
))
1140 def terminate(self
):
1141 # Stop running FRR daemons
1144 # Disable forwarding
1145 set_sysctl(self
, "net.ipv4.ip_forward", 0)
1146 set_sysctl(self
, "net.ipv6.conf.all.forwarding", 0)
1147 super(Router
, self
).terminate()
1148 os
.system("chmod -R go+rw /tmp/topotests")
1150 # Return count of running daemons
1151 def listDaemons(self
):
1153 rundaemons
= self
.cmd("ls -1 /var/run/%s/*.pid" % self
.routertype
)
1155 if re
.search(r
"No such file or directory", rundaemons
):
1157 if rundaemons
is not None:
1158 bet
= rundaemons
.split("\n")
1160 daemonpid
= self
.cmd("cat %s" % d
.rstrip()).rstrip()
1161 if daemonpid
.isdigit() and pid_exists(int(daemonpid
)):
1162 ret
.append(os
.path
.basename(d
.rstrip().rsplit(".", 1)[0]))
1166 def stopRouter(self
, wait
=True, assertOnError
=True, minErrorVersion
="5.1"):
1167 # Stop Running FRR Daemons
1168 rundaemons
= self
.cmd("ls -1 /var/run/%s/*.pid" % self
.routertype
)
1170 if re
.search(r
"No such file or directory", rundaemons
):
1172 if rundaemons
is not None:
1173 dmns
= rundaemons
.split("\n")
1174 # Exclude empty string at end of list
1176 daemonpid
= self
.cmd("cat %s" % d
.rstrip()).rstrip()
1177 if daemonpid
.isdigit() and pid_exists(int(daemonpid
)):
1178 daemonname
= os
.path
.basename(d
.rstrip().rsplit(".", 1)[0])
1179 logger
.info("{}: stopping {}".format(self
.name
, daemonname
))
1181 os
.kill(int(daemonpid
), signal
.SIGTERM
)
1182 except OSError as err
:
1183 if err
.errno
== errno
.ESRCH
:
1185 "{}: {} left a dead pidfile (pid={})".format(
1186 self
.name
, daemonname
, daemonpid
1191 "{}: {} could not kill pid {}: {}".format(
1192 self
.name
, daemonname
, daemonpid
, str(err
)
1199 running
= self
.listDaemons()
1204 "{}: waiting for daemons stopping: {}".format(
1205 self
.name
, ", ".join(running
)
1208 running
= self
.listDaemons()
1211 while counter
> 0 and running
:
1214 "{}: waiting for daemons stopping: {}".format(
1215 self
.name
, ", ".join(running
)
1218 running
= self
.listDaemons()
1222 # 2nd round of kill if daemons didn't exit
1223 dmns
= rundaemons
.split("\n")
1224 # Exclude empty string at end of list
1226 daemonpid
= self
.cmd("cat %s" % d
.rstrip()).rstrip()
1227 if daemonpid
.isdigit() and pid_exists(int(daemonpid
)):
1229 "{}: killing {}".format(
1231 os
.path
.basename(d
.rstrip().rsplit(".", 1)[0]),
1234 self
.cmd("kill -7 %s" % daemonpid
)
1236 self
.cmd("rm -- {}".format(d
.rstrip()))
1241 errors
= self
.checkRouterCores(reportOnce
=True)
1242 if self
.checkRouterVersion("<", minErrorVersion
):
1243 # ignore errors in old versions
1245 if assertOnError
and errors
is not None and len(errors
) > 0:
1246 assert "Errors found - details follow:" == 0, errors
1249 def removeIPs(self
):
1250 for interface
in self
.intfNames():
1251 self
.cmd("ip address flush", interface
)
1253 def checkCapability(self
, daemon
, param
):
1254 if param
is not None:
1255 daemon_path
= os
.path
.join(self
.daemondir
, daemon
)
1256 daemon_search_option
= param
.replace("-", "")
1258 "{0} -h | grep {1}".format(daemon_path
, daemon_search_option
)
1260 if daemon_search_option
not in output
:
1264 def loadConf(self
, daemon
, source
=None, param
=None):
1265 # print "Daemons before:", self.daemons
1266 if daemon
in self
.daemons
.keys():
1267 self
.daemons
[daemon
] = 1
1268 if param
is not None:
1269 self
.daemons_options
[daemon
] = param
1271 self
.cmd("touch /etc/%s/%s.conf" % (self
.routertype
, daemon
))
1274 self
.cmd("cp %s /etc/%s/%s.conf" % (source
, self
.routertype
, daemon
))
1276 self
.cmd("chmod 640 /etc/%s/%s.conf" % (self
.routertype
, daemon
))
1279 "chown %s:%s /etc/%s/%s.conf"
1280 % (self
.routertype
, self
.routertype
, self
.routertype
, daemon
)
1283 if (daemon
== "zebra") and (self
.daemons
["staticd"] == 0):
1284 # Add staticd with zebra - if it exists
1285 staticd_path
= os
.path
.join(self
.daemondir
, "staticd")
1286 if os
.path
.isfile(staticd_path
):
1287 self
.daemons
["staticd"] = 1
1288 self
.daemons_options
["staticd"] = ""
1289 # Auto-Started staticd has no config, so it will read from zebra config
1291 logger
.info("No daemon {} known".format(daemon
))
1292 # print "Daemons after:", self.daemons
1294 def startRouter(self
, tgen
=None):
1295 # Disable integrated-vtysh-config
1297 'echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf'
1301 "chown %s:%svty /etc/%s/vtysh.conf"
1302 % (self
.routertype
, self
.routertype
, self
.routertype
)
1304 # TODO remove the following lines after all tests are migrated to Topogen.
1305 # Try to find relevant old logfiles in /tmp and delete them
1306 map(os
.remove
, glob
.glob("{}/{}/*.log".format(self
.logdir
, self
.name
)))
1307 # Remove old core files
1308 map(os
.remove
, glob
.glob("{}/{}/*.dmp".format(self
.logdir
, self
.name
)))
1309 # Remove IP addresses from OS first - we have them in zebra.conf
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
1313 if self
.daemons
["ldpd"] == 1:
1314 ldpd_path
= os
.path
.join(self
.daemondir
, "ldpd")
1315 if not os
.path
.isfile(ldpd_path
):
1316 logger
.info("LDP Test, but no ldpd compiled or installed")
1317 return "LDP Test, but no ldpd compiled or installed"
1319 if version_cmp(platform
.release(), "4.5") < 0:
1320 logger
.info("LDP Test need Linux Kernel 4.5 minimum")
1321 return "LDP Test need Linux Kernel 4.5 minimum"
1322 # Check if have mpls
1324 self
.hasmpls
= tgen
.hasmpls
1325 if self
.hasmpls
!= True:
1327 "LDP/MPLS Tests will be skipped, platform missing module(s)"
1330 # Test for MPLS Kernel modules available
1331 self
.hasmpls
= False
1332 if not module_present("mpls-router"):
1334 "MPLS tests will not run (missing mpls-router kernel module)"
1336 elif not module_present("mpls-iptunnel"):
1338 "MPLS tests will not run (missing mpls-iptunnel kernel module)"
1342 if self
.hasmpls
!= True:
1343 return "LDP/MPLS Tests need mpls kernel modules"
1344 self
.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels")
1346 if self
.daemons
["eigrpd"] == 1:
1347 eigrpd_path
= os
.path
.join(self
.daemondir
, "eigrpd")
1348 if not os
.path
.isfile(eigrpd_path
):
1349 logger
.info("EIGRP Test, but no eigrpd compiled or installed")
1350 return "EIGRP Test, but no eigrpd compiled or installed"
1352 if self
.daemons
["bfdd"] == 1:
1353 bfdd_path
= os
.path
.join(self
.daemondir
, "bfdd")
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"
1358 return self
.startRouterDaemons()
1360 def getStdErr(self
, daemon
):
1361 return self
.getLog("err", daemon
)
1363 def getStdOut(self
, daemon
):
1364 return self
.getLog("out", daemon
)
1366 def getLog(self
, log
, daemon
):
1367 return self
.cmd("cat {}/{}/{}.{}".format(self
.logdir
, self
.name
, daemon
, log
))
1369 def startRouterDaemons(self
, daemons
=None):
1370 "Starts all FRR daemons for this router."
1374 if os
.path
.exists("/etc/frr/support_bundle_commands.conf"):
1375 bundle_data
= subprocess
.check_output(
1376 ["cat /etc/frr/support_bundle_commands.conf"], shell
=True
1379 "echo '{}' > /etc/frr/support_bundle_commands.conf".format(bundle_data
)
1382 # Starts actual daemons without init (ie restart)
1383 # cd to per node directory
1384 self
.cmd("install -d {}/{}".format(self
.logdir
, self
.name
))
1385 self
.cmd("cd {}/{}".format(self
.logdir
, self
.name
))
1386 self
.cmd("umask 000")
1388 # Re-enable to allow for report per run
1389 self
.reportCores
= True
1391 # XXX: glue code forward ported from removed function.
1392 if self
.version
== None:
1393 self
.version
= self
.cmd(
1394 os
.path
.join(self
.daemondir
, "bgpd") + " -v"
1396 logger
.info("{}: running version: {}".format(self
.name
, self
.version
))
1398 # If `daemons` was specified then some upper API called us with
1399 # specific daemons, otherwise just use our own configuration.
1402 daemons_list
= daemons
1404 # Append all daemons configured.
1405 for daemon
in self
.daemons
:
1406 if self
.daemons
[daemon
] == 1:
1407 daemons_list
.append(daemon
)
1410 if "zebra" in daemons_list
:
1411 zebra_path
= os
.path
.join(self
.daemondir
, "zebra")
1412 zebra_option
= self
.daemons_options
["zebra"]
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(
1415 zebra_path
, zebra_option
, self
.logdir
, self
.name
1418 logger
.debug("{}: {} zebra started".format(self
, self
.routertype
))
1420 # Remove `zebra` so we don't attempt to start it again.
1421 while "zebra" in daemons_list
:
1422 daemons_list
.remove("zebra")
1424 # Start staticd next if required
1425 if "staticd" in daemons_list
:
1426 staticd_path
= os
.path
.join(self
.daemondir
, "staticd")
1427 staticd_option
= self
.daemons_options
["staticd"]
1429 "ASAN_OPTIONS=log_path=staticd.asan {0} {1} --log file:staticd.log --log-level debug -d > staticd.out 2> staticd.err".format(
1430 staticd_path
, staticd_option
, self
.logdir
, self
.name
1433 logger
.debug("{}: {} staticd started".format(self
, self
.routertype
))
1435 # Remove `staticd` so we don't attempt to start it again.
1436 while "staticd" in daemons_list
:
1437 daemons_list
.remove("staticd")
1439 # Fix Link-Local Addresses
1440 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
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"
1445 # Now start all the other daemons
1446 for daemon
in daemons_list
:
1447 # Skip disabled daemons and zebra
1448 if self
.daemons
[daemon
] == 0:
1451 daemon_path
= os
.path
.join(self
.daemondir
, daemon
)
1453 "ASAN_OPTIONS=log_path={2}.asan {0} {1} --log file:{2}.log --log-level debug -d > {2}.out 2> {2}.err".format(
1454 daemon_path
, self
.daemons_options
.get(daemon
, ""), daemon
1457 logger
.debug("{}: {} {} started".format(self
, self
.routertype
, daemon
))
1459 # Check if daemons are running.
1460 rundaemons
= self
.cmd("ls -1 /var/run/%s/*.pid" % self
.routertype
)
1461 if re
.search(r
"No such file or directory", rundaemons
):
1462 return "Daemons are not running"
1466 def killRouterDaemons(
1467 self
, daemons
, wait
=True, assertOnError
=True, minErrorVersion
="5.1"
1470 # Daemons(user specified daemon only) using SIGKILL
1471 rundaemons
= self
.cmd("ls -1 /var/run/%s/*.pid" % self
.routertype
)
1473 daemonsNotRunning
= []
1474 if re
.search(r
"No such file or directory", rundaemons
):
1476 for daemon
in daemons
:
1477 if rundaemons
is not None and daemon
in rundaemons
:
1479 dmns
= rundaemons
.split("\n")
1480 # Exclude empty string at end of list
1482 if re
.search(r
"%s" % daemon
, d
):
1483 daemonpid
= self
.cmd("cat %s" % d
.rstrip()).rstrip()
1484 if daemonpid
.isdigit() and pid_exists(int(daemonpid
)):
1486 "{}: killing {}".format(
1488 os
.path
.basename(d
.rstrip().rsplit(".", 1)[0]),
1491 self
.cmd("kill -9 %s" % daemonpid
)
1493 if pid_exists(int(daemonpid
)):
1495 if wait
and numRunning
> 0:
1498 "{}: waiting for {} daemon to be stopped".format(
1503 # 2nd round of kill if daemons didn't exit
1505 if re
.search(r
"%s" % daemon
, d
):
1506 daemonpid
= self
.cmd("cat %s" % d
.rstrip()).rstrip()
1507 if daemonpid
.isdigit() and pid_exists(
1511 "{}: killing {}".format(
1514 d
.rstrip().rsplit(".", 1)[0]
1518 self
.cmd("kill -9 %s" % daemonpid
)
1520 self
.cmd("rm -- {}".format(d
.rstrip()))
1522 errors
= self
.checkRouterCores(reportOnce
=True)
1523 if self
.checkRouterVersion("<", minErrorVersion
):
1524 # ignore errors in old versions
1526 if assertOnError
and len(errors
) > 0:
1527 assert "Errors found - details follow:" == 0, errors
1529 daemonsNotRunning
.append(daemon
)
1530 if len(daemonsNotRunning
) > 0:
1531 errors
= errors
+ "Daemons are not running", daemonsNotRunning
1535 def checkRouterCores(self
, reportLeaks
=True, reportOnce
=False):
1536 if reportOnce
and not self
.reportCores
:
1540 for daemon
in self
.daemons
:
1541 if self
.daemons
[daemon
] == 1:
1542 # Look for core file
1543 corefiles
= glob
.glob(
1544 "{}/{}/{}_core*.dmp".format(self
.logdir
, self
.name
, daemon
)
1546 if len(corefiles
) > 0:
1547 backtrace
= gdb_core(self
, daemon
, corefiles
)
1550 + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s"
1551 % (self
.name
, daemon
, backtrace
)
1555 log
= self
.getStdErr(daemon
)
1556 if "memstats" in log
:
1558 "%s: %s has memory leaks:\n" % (self
.name
, daemon
)
1560 traces
= traces
+ "\n%s: %s has memory leaks:\n" % (
1564 log
= re
.sub("core_handler: ", "", log
)
1566 r
"(showing active allocations in memory group [a-zA-Z0-9]+)",
1570 log
= re
.sub("memstats: ", " ", log
)
1571 sys
.stderr
.write(log
)
1573 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
1574 if checkAddressSanitizerError(
1575 self
.getStdErr(daemon
), self
.name
, daemon
, self
.logdir
1578 "%s: Daemon %s killed by AddressSanitizer" % (self
.name
, daemon
)
1580 traces
= traces
+ "\n%s: Daemon %s killed by AddressSanitizer" % (
1586 self
.reportCores
= False
1589 def checkRouterRunning(self
):
1590 "Check if router daemons are running and collect crashinfo they don't run"
1594 daemonsRunning
= self
.cmd(
1595 'vtysh -c "show logging" | grep "Logging configuration for"'
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
)
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
))
1604 if daemon
== "staticd":
1606 "You may have a copy of staticd installed but are attempting to test against\n"
1609 "a version of FRR that does not have staticd, please cleanup the install dir\n"
1612 # Look for core file
1613 corefiles
= glob
.glob(
1614 "{}/{}/{}_core*.dmp".format(self
.logdir
, self
.name
, daemon
)
1616 if len(corefiles
) > 0:
1617 gdb_core(self
, daemon
, corefiles
)
1619 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
1621 "{}/{}/{}.log".format(self
.logdir
, self
.name
, daemon
)
1623 log_tail
= subprocess
.check_output(
1625 "tail -n20 {}/{}/{}.log 2> /dev/null".format(
1626 self
.logdir
, self
.name
, daemon
1632 "\nFrom %s %s %s log file:\n"
1633 % (self
.routertype
, self
.name
, daemon
)
1635 sys
.stderr
.write("%s\n" % log_tail
)
1637 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
1638 if checkAddressSanitizerError(
1639 self
.getStdErr(daemon
), self
.name
, daemon
, self
.logdir
1641 return "%s: Daemon %s not running - killed by AddressSanitizer" % (
1646 return "%s: Daemon %s not running" % (self
.name
, daemon
)
1649 def checkRouterVersion(self
, cmpop
, version
):
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
1659 Usage example: router.checkRouterVersion('>', '1.0')
1662 # Make sure we have version information first
1663 if self
.version
== None:
1664 self
.version
= self
.cmd(
1665 os
.path
.join(self
.daemondir
, "bgpd") + " -v"
1667 logger
.info("{}: running version: {}".format(self
.name
, self
.version
))
1669 rversion
= self
.version
1670 if rversion
== None:
1673 result
= version_cmp(rversion
, version
)
1687 def get_ipv6_linklocal(self
):
1688 "Get LinkLocal Addresses from interfaces"
1692 ifaces
= self
.cmd("ip -6 address")
1693 # Fix newlines (make them all the same)
1694 ifaces
= ("\n".join(ifaces
.splitlines()) + "\n").splitlines()
1698 m
= re
.search("[0-9]+: ([^:@]+)[@if0-9:]+ <", line
)
1700 interface
= m
.group(1)
1703 "inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link",
1708 ll_per_if_count
+= 1
1709 if ll_per_if_count
> 1:
1710 linklocal
+= [["%s-%s" % (interface
, ll_per_if_count
), local
]]
1712 linklocal
+= [[interface
, local
]]
1715 def daemon_available(self
, daemon
):
1716 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
1718 daemon_path
= os
.path
.join(self
.daemondir
, daemon
)
1719 if not os
.path
.isfile(daemon_path
):
1721 if daemon
== "ldpd":
1722 if version_cmp(platform
.release(), "4.5") < 0:
1724 if not module_present("mpls-router", load
=False):
1726 if not module_present("mpls-iptunnel", load
=False):
1730 def get_routertype(self
):
1731 "Return the type of Router (frr)"
1733 return self
.routertype
1735 def report_memory_leaks(self
, filename_prefix
, testscript
):
1736 "Report Memory Leaks to file prefixed with given string"
1739 filename
= filename_prefix
+ re
.sub(r
"\.py", "", testscript
) + ".txt"
1740 for daemon
in self
.daemons
:
1741 if self
.daemons
[daemon
] == 1:
1742 log
= self
.getStdErr(daemon
)
1743 if "memstats" in log
:
1746 "\nRouter {} {} StdErr Log:\n{}".format(self
.name
, daemon
, log
)
1750 # Check if file already exists
1751 fileexists
= os
.path
.isfile(filename
)
1752 leakfile
= open(filename
, "a")
1754 # New file - add header
1756 "# Memory Leak Detection for topotest %s\n\n"
1759 leakfile
.write("## Router %s\n" % self
.name
)
1760 leakfile
.write("### Process %s\n" % daemon
)
1761 log
= re
.sub("core_handler: ", "", log
)
1763 r
"(showing active allocations in memory group [a-zA-Z0-9]+)",
1767 log
= re
.sub("memstats: ", " ", log
)
1769 leakfile
.write("\n")
1774 class LinuxRouter(Router
):
1775 "A Linux Router Node with IPv4/IPv6 forwarding enabled."
1777 def __init__(self
, name
, **params
):
1778 Router
.__init
__(self
, name
, **params
)
1780 def config(self
, **params
):
1781 Router
.config(self
, **params
)
1782 # Enable forwarding on the router
1783 assert_sysctl(self
, "net.ipv4.ip_forward", 1)
1784 assert_sysctl(self
, "net.ipv6.conf.all.forwarding", 1)
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
1790 # |/usr/share/apport/apport %p %s %c %d %P
1791 corefile
= "%e_core-sig_%s-pid_%p.dmp"
1792 assert_sysctl(self
, "kernel.core_pattern", corefile
)
1794 def terminate(self
):
1796 Terminate generic LinuxRouter Mininet instance
1798 set_sysctl(self
, "net.ipv4.ip_forward", 0)
1799 set_sysctl(self
, "net.ipv6.conf.all.forwarding", 0)
1800 Router
.terminate(self
)
1803 class FreeBSDRouter(Router
):
1804 "A FreeBSD Router Node with IPv4/IPv6 forwarding enabled."
1806 def __init__(eslf
, name
, **params
):
1807 Router
.__init
__(Self
, name
, **params
)
1810 class LegacySwitch(OVSSwitch
):
1811 "A Legacy Switch without OpenFlow"
1813 def __init__(self
, name
, **params
):
1814 OVSSwitch
.__init
__(self
, name
, failMode
="standalone", **params
)
1815 self
.switchIP
= None
1819 """Convert string to unicode, depending on python version"""
1820 if sys
.version_info
[0] > 2: