diff options
Diffstat (limited to 'tests/topotests')
19 files changed, 687 insertions, 56 deletions
diff --git a/tests/topotests/all_protocol_startup/r1/ip_nht.ref b/tests/topotests/all_protocol_startup/r1/ip_nht.ref index 3592f29b5..2b4363b69 100644 --- a/tests/topotests/all_protocol_startup/r1/ip_nht.ref +++ b/tests/topotests/all_protocol_startup/r1/ip_nht.ref @@ -1,35 +1,35 @@ VRF default: Resolve via default: on 1.1.1.1 - resolved via static + resolved via static, prefix 1.1.1.1/32 is directly connected, r1-eth1 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.2 - resolved via static + resolved via static, prefix 1.1.1.2/32 is directly connected, r1-eth2 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.3 - resolved via static + resolved via static, prefix 1.1.1.3/32 is directly connected, r1-eth3 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.4 - resolved via static + resolved via static, prefix 1.1.1.4/32 is directly connected, r1-eth4 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.5 - resolved via static + resolved via static, prefix 1.1.1.5/32 is directly connected, r1-eth5 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.6 - resolved via static + resolved via static, prefix 1.1.1.6/32 is directly connected, r1-eth6 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.7 - resolved via static + resolved via static, prefix 1.1.1.7/32 is directly connected, r1-eth7 (vrf default), weight 1 Client list: pbr(fd XX) 1.1.1.8 - resolved via static + resolved via static, prefix 1.1.1.8/32 is directly connected, r1-eth8 (vrf default), weight 1 Client list: pbr(fd XX) 2.2.2.1 @@ -54,19 +54,19 @@ VRF default: unresolved Client list: pbr(fd XX) 192.168.0.2 - resolved via connected + resolved via connected, prefix 192.168.0.0/24 is directly connected, r1-eth0 (vrf default), weight 1 Client list: static(fd XX) 192.168.0.4 - resolved via connected + resolved via connected, prefix 192.168.0.0/24 is directly connected, r1-eth0 (vrf default), weight 1 Client list: static(fd XX) 192.168.7.10 - resolved via connected + resolved via connected, prefix 192.168.7.0/26 is directly connected, r1-eth7 (vrf default), weight 1 Client list: bgp(fd XX) 192.168.7.20(Connected) - resolved via connected + resolved via connected, prefix 192.168.7.0/26 is directly connected, r1-eth7 (vrf default), weight 1 Client list: bgp(fd XX) 192.168.161.4 diff --git a/tests/topotests/all_protocol_startup/r1/ipv6_nht.ref b/tests/topotests/all_protocol_startup/r1/ipv6_nht.ref index 7b7176118..3f03d6fe9 100644 --- a/tests/topotests/all_protocol_startup/r1/ipv6_nht.ref +++ b/tests/topotests/all_protocol_startup/r1/ipv6_nht.ref @@ -1,15 +1,15 @@ VRF default: Resolve via default: on fc00::2 - resolved via connected + resolved via connected, prefix fc00::/64 is directly connected, r1-eth0 (vrf default), weight 1 Client list: static(fd XX) fc00:0:0:8::1000 - resolved via connected + resolved via connected, prefix fc00:0:0:8::/64 is directly connected, r1-eth8 (vrf default), weight 1 Client list: bgp(fd XX) fc00:0:0:8::2000(Connected) - resolved via connected + resolved via connected, prefix fc00:0:0:8::/64 is directly connected, r1-eth8 (vrf default), weight 1 Client list: bgp(fd XX) diff --git a/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py b/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py index 63f1719e1..a5232ad69 100644 --- a/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py +++ b/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py @@ -108,7 +108,7 @@ def test_bgp_set_aspath_exclude(): pytest.skip(tgen.errors) test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_1) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed overriding incoming AS-PATH with route-map" @@ -128,7 +128,6 @@ def test_bgp_set_aspath_exclude_access_list(): conf bgp as-path access-list FIRST permit ^65 route-map r2 permit 6 - no set as-path exclude as-path-access-list SECOND set as-path exclude as-path-access-list FIRST """ ) @@ -140,21 +139,20 @@ clear bgp * ) test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_2) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed change of exclude rule in route map" r1.vtysh_cmd( """ conf route-map r2 permit 6 - no set as-path exclude as-path-access-list FIRST set as-path exclude as-path-access-list SECOND """ ) # tgen.mininet_cli() test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_1) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed reverting exclude rule in route map" @@ -182,7 +180,7 @@ clear bgp * ) test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_3) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed to removing current accesslist" @@ -200,7 +198,7 @@ clear bgp * ) test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_4) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed to renegotiate with peers 2" @@ -208,7 +206,7 @@ clear bgp * """ conf route-map r2 permit 6 - no set as-path exclude as-path-access-list SECOND + set as-path exclude 65555 """ ) @@ -219,7 +217,26 @@ clear bgp * ) test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_3) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) + + assert result is None, "Failed to renegotiate with peers 2" + + r1.vtysh_cmd( + """ +conf + route-map r2 permit 6 + set as-path exclude as-path-access-list NON-EXISTING + """ + ) + + r1.vtysh_cmd( + """ +clear bgp * + """ + ) + + test_func = functools.partial(bgp_converge, tgen.gears["r1"], expected_3) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) assert result is None, "Failed to renegotiate with peers 2" diff --git a/tests/topotests/conftest.py b/tests/topotests/conftest.py index be28b388b..44536e945 100755 --- a/tests/topotests/conftest.py +++ b/tests/topotests/conftest.py @@ -18,12 +18,11 @@ from pathlib import Path import lib.fixtures import pytest from lib.common_config import generate_support_bundle -from lib.micronet_compat import Mininet from lib.topogen import diagnose_env, get_topogen from lib.topolog import get_test_logdir, logger from lib.topotest import json_cmp_result from munet import cli -from munet.base import Commander, proc_error +from munet.base import BaseMunet, Commander, proc_error from munet.cleanup import cleanup_current, cleanup_previous from munet.config import ConfigOptionsProxy from munet.testing.util import pause_test @@ -86,7 +85,7 @@ def pytest_addoption(parser): parser.addoption( "--cli-on-error", action="store_true", - help="Mininet cli on test failure", + help="Munet cli on test failure", ) parser.addoption( @@ -711,7 +710,7 @@ def pytest_runtest_makereport(item, call): wait_for_procs = [] # Really would like something better than using this global here. # Not all tests use topogen though so get_topogen() won't work. - for node in Mininet.g_mnet_inst.hosts.values(): + for node in BaseMunet.g_unet.hosts.values(): pause = True if is_tmux: @@ -720,13 +719,15 @@ def pytest_runtest_makereport(item, call): if not isatty else None ) - Commander.tmux_wait_gen += 1 - wait_for_channels.append(channel) + # If we don't have a tty to pause on pause for tmux windows to exit + if channel is not None: + Commander.tmux_wait_gen += 1 + wait_for_channels.append(channel) pane_info = node.run_in_window( error_cmd, new_window=win_info is None, - background=True, + background=not isatty, title="{} ({})".format(title, node.name), name=title, tmux_target=win_info, @@ -737,9 +738,13 @@ def pytest_runtest_makereport(item, call): win_info = pane_info elif is_xterm: assert isinstance(pane_info, subprocess.Popen) - wait_for_procs.append(pane_info) + # If we don't have a tty to pause on pause for xterm procs to exit + if not isatty: + wait_for_procs.append(pane_info) # Now wait on any channels + if wait_for_channels or wait_for_procs: + logger.info("Pausing for error command windows to exit") for channel in wait_for_channels: logger.debug("Waiting on TMUX channel %s", channel) commander.cmd_raises([commander.get_exec_path("tmux"), "wait", channel]) @@ -752,10 +757,10 @@ def pytest_runtest_makereport(item, call): if error and item.config.option.cli_on_error: # Really would like something better than using this global here. # Not all tests use topogen though so get_topogen() won't work. - if Mininet.g_mnet_inst: - cli.cli(Mininet.g_mnet_inst, title=title, background=False) + if BaseMunet.g_unet: + cli.cli(BaseMunet.g_unet, title=title, background=False) else: - logger.error("Could not launch CLI b/c no mininet exists yet") + logger.error("Could not launch CLI b/c no munet exists yet") if pause and isatty: pause_test() @@ -800,9 +805,20 @@ done""" def pytest_terminal_summary(terminalreporter, exitstatus, config): # Only run if we are the top level test runner is_xdist_worker = "PYTEST_XDIST_WORKER" in os.environ + is_xdist = os.environ["PYTEST_XDIST_MODE"] != "no" if config.option.cov_topotest and not is_xdist_worker: coverage_finish(terminalreporter, config) + if ( + is_xdist + and not is_xdist_worker + and ( + bool(config.getoption("--pause")) + or bool(config.getoption("--pause-at-end")) + ) + ): + pause_test("pause-at-end") + # # Add common fixtures available to all tests as parameters diff --git a/tests/topotests/lib/pim.py b/tests/topotests/lib/pim.py index 71e36b622..eb3723be4 100644 --- a/tests/topotests/lib/pim.py +++ b/tests/topotests/lib/pim.py @@ -1607,7 +1607,7 @@ def verify_pim_rp_info( if type(group_addresses) is not list: group_addresses = [group_addresses] - if type(oif) is not list: + if oif is not None and type(oif) is not list: oif = [oif] for grp in group_addresses: diff --git a/tests/topotests/lib/topotest.py b/tests/topotests/lib/topotest.py index 5a8c2e596..dc6107bbe 100644 --- a/tests/topotests/lib/topotest.py +++ b/tests/topotests/lib/topotest.py @@ -396,6 +396,9 @@ def run_and_expect(func, what, count=20, wait=3): waiting `wait` seconds between tries. By default it tries 20 times with 3 seconds delay between tries. + Changing default count/wait values, please change them below also for + `minimum_wait`, and `minimum_count`. + Returns (True, func-return) on success or (False, func-return) on failure. @@ -414,13 +417,18 @@ def run_and_expect(func, what, count=20, wait=3): # Just a safety-check to avoid running topotests with very # small wait/count arguments. + # If too low count/wait values are defined, override them + # with the minimum values. + minimum_count = 20 + minimum_wait = 3 + minimum_wait_time = 15 # The overall minimum seconds for the test to wait wait_time = wait * count - if wait_time < 5: - assert ( - wait_time >= 5 - ), "Waiting time is too small (count={}, wait={}), adjust timer values".format( - count, wait + if wait_time < minimum_wait_time: + logger.warn( + f"Waiting time is too small (count={count}, wait={wait}), using default values (count={minimum_count}, wait={minimum_wait})" ) + count = minimum_count + wait = minimum_wait logger.debug( "'{}' polling started (interval {} secs, maximum {} tries)".format( diff --git a/tests/topotests/mgmt_oper/r1/frr-yanglib.conf b/tests/topotests/mgmt_oper/r1/frr-yanglib.conf new file mode 100644 index 000000000..f37766b15 --- /dev/null +++ b/tests/topotests/mgmt_oper/r1/frr-yanglib.conf @@ -0,0 +1,10 @@ +log timestamp precision 6 +log file frr.log + +no debug memstats-at-exit + +debug mgmt backend datastore frontend transaction + +interface r1-eth0 + ip address 1.1.1.1/24 +exit diff --git a/tests/topotests/mgmt_oper/test_yanglib.py b/tests/topotests/mgmt_oper/test_yanglib.py new file mode 100644 index 000000000..e094ca544 --- /dev/null +++ b/tests/topotests/mgmt_oper/test_yanglib.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# September 17 2024, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 2024, LabN Consulting, L.L.C. +# + +import json +import pytest +from lib.topogen import Topogen + +pytestmark = [pytest.mark.staticd, pytest.mark.mgmtd] + + +@pytest.fixture(scope="module") +def tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + + topodef = {"s1": ("r1",)} + + tgen = Topogen(topodef, request.module.__name__) + tgen.start_topology() + + router_list = tgen.routers() + for rname, router in router_list.items(): + router.load_frr_config("frr-yanglib.conf") + + tgen.start_router() + yield tgen + tgen.stop_topology() + + +def test_yang_lib(tgen): + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.gears["r1"].net + output = r1.cmd_nostatus( + "vtysh -c 'show mgmt get-data /ietf-yang-library:yang-library'" + ) + ret = json.loads(output) + loaded_modules = ret['ietf-yang-library:yang-library']['module-set'][0]['module'] + assert len(loaded_modules) > 10, "Modules missing from yang-library" diff --git a/tests/topotests/munet/cli.py b/tests/topotests/munet/cli.py index 01a709151..d273a30ea 100644 --- a/tests/topotests/munet/cli.py +++ b/tests/topotests/munet/cli.py @@ -745,7 +745,7 @@ async def cli_client_connected(unet, background, reader, writer): await writer.drain() -async def remote_cli(unet, prompt, title, background): +async def remote_cli(unet, prompt, title, background, remote_wait=False): """Open a CLI in a new window.""" try: if not unet.cli_sockpath: @@ -756,6 +756,13 @@ async def remote_cli(unet, prompt, title, background): unet.cli_sockpath = sockpath logging.info("server created on :\n%s\n", sockpath) + if remote_wait: + wait_tmux = bool(os.getenv("TMUX", "")) + wait_x11 = not wait_tmux and bool(os.getenv("DISPLAY", "")) + else: + wait_tmux = False + wait_x11 = False + # Open a new window with a new CLI python_path = await unet.async_get_exec_path(["python3", "python"]) us = os.path.realpath(__file__) @@ -765,7 +772,32 @@ async def remote_cli(unet, prompt, title, background): if prompt: cmd += f" --prompt='{prompt}'" cmd += " " + unet.cli_sockpath - unet.run_in_window(cmd, title=title, background=False) + + channel = None + if wait_tmux: + from .base import Commander # pylint: disable=import-outside-toplevel + + channel = "{}-{}".format(os.getpid(), Commander.tmux_wait_gen) + logger.info("XXX channel is %s", channel) + # If we don't have a tty to pause on pause for tmux windows to exit + if channel is not None: + Commander.tmux_wait_gen += 1 + + pane_info = unet.run_in_window( + cmd, title=title, background=False, wait_for=channel + ) + + if wait_tmux and channel: + from .base import commander # pylint: disable=import-outside-toplevel + + logger.debug("Waiting on TMUX CLI window") + await commander.async_cmd_raises( + [commander.get_exec_path("tmux"), "wait", channel] + ) + elif wait_x11 and isinstance(pane_info, subprocess.Popen): + logger.debug("Waiting on xterm CLI process %s", pane_info) + if hasattr(asyncio, "to_thread"): + await asyncio.to_thread(pane_info.wait) # pylint: disable=no-member except Exception as error: logging.error("cli server: unexpected exception: %s", error) @@ -906,8 +938,22 @@ def cli( prompt=None, background=True, ): + # In the case of no tty a remote_cli will be used, and we want it to wait on finish + # of the spawned cli.py script, otherwise it returns back here and exits async loop + # which kills the server side CLI socket operation. + remote_wait = not sys.stdin.isatty() + asyncio.run( - async_cli(unet, histfile, sockpath, force_window, title, prompt, background) + async_cli( + unet, + histfile, + sockpath, + force_window, + title, + prompt, + background, + remote_wait=remote_wait, + ) ) @@ -919,12 +965,14 @@ async def async_cli( title=None, prompt=None, background=True, + remote_wait=False, ): if prompt is None: prompt = "munet> " if force_window or not sys.stdin.isatty(): - await remote_cli(unet, prompt, title, background) + await remote_cli(unet, prompt, title, background, remote_wait) + return if not unet: logger.debug("client-cli using sockpath %s", sockpath) diff --git a/tests/topotests/munet/native.py b/tests/topotests/munet/native.py index b7c6e4a63..e3b782396 100644 --- a/tests/topotests/munet/native.py +++ b/tests/topotests/munet/native.py @@ -2733,7 +2733,7 @@ ff02::2\tip6-allrouters ), "format": "stdout HOST [HOST ...]", "help": "tail -f on the stdout of the qemu/cmd for this node", - "new-window": True, + "new-window": {"background": True, "ns_only": True}, }, { "name": "stderr", @@ -2743,7 +2743,7 @@ ff02::2\tip6-allrouters ), "format": "stderr HOST [HOST ...]", "help": "tail -f on the stdout of the qemu/cmd for this node", - "new-window": True, + "new-window": {"background": True, "ns_only": True}, }, ] } diff --git a/tests/topotests/munet/testing/util.py b/tests/topotests/munet/testing/util.py index a1a94bcd1..99687c0a8 100644 --- a/tests/topotests/munet/testing/util.py +++ b/tests/topotests/munet/testing/util.py @@ -52,12 +52,13 @@ def pause_test(desc=""): asyncio.run(async_pause_test(desc)) -def retry(retry_timeout, initial_wait=0, expected=True): +def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True): """decorator: retry while functions return is not None or raises an exception. * `retry_timeout`: Retry for at least this many seconds; after waiting initial_wait seconds * `initial_wait`: Sleeps for this many seconds before first executing function + * `retry_sleep`: The time to sleep between retries. * `expected`: if False then the return logic is inverted, except for exceptions, (i.e., a non None ends the retry loop, and returns that value) """ @@ -65,9 +66,8 @@ def retry(retry_timeout, initial_wait=0, expected=True): def _retry(func): @functools.wraps(func) def func_retry(*args, **kwargs): - retry_sleep = 2 - # Allow the wrapped function's args to override the fixtures + _retry_sleep = float(kwargs.pop("retry_sleep", retry_sleep)) _retry_timeout = kwargs.pop("retry_timeout", retry_timeout) _expected = kwargs.pop("expected", expected) _initial_wait = kwargs.pop("initial_wait", initial_wait) @@ -82,13 +82,21 @@ def retry(retry_timeout, initial_wait=0, expected=True): while True: seconds_left = (retry_until - datetime.datetime.now()).total_seconds() try: - ret = func(*args, **kwargs) - if _expected and ret is None: + try: + ret = func(*args, seconds_left=seconds_left, **kwargs) + except TypeError as error: + if "seconds_left" not in str(error): + raise + ret = func(*args, **kwargs) + + logging.debug("Function returned %s", ret) + + positive_result = ret is None + if _expected == positive_result: logging.debug("Function succeeds") return ret - logging.debug("Function returned %s", ret) except Exception as error: - logging.info("Function raised exception: %s", str(error)) + logging.info('Function raised exception: "%s"', error) ret = error if seconds_left < 0: @@ -99,10 +107,10 @@ def retry(retry_timeout, initial_wait=0, expected=True): logging.info( "Sleeping %ds until next retry with %.1f retry time left", - retry_sleep, + _retry_sleep, seconds_left, ) - time.sleep(retry_sleep) + time.sleep(_retry_sleep) func_retry._original = func # pylint: disable=W0212 return func_retry diff --git a/tests/topotests/pim_cand_rp_bsr/__init__.py b/tests/topotests/pim_cand_rp_bsr/__init__.py new file mode 100755 index 000000000..e69de29bb --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/__init__.py diff --git a/tests/topotests/pim_cand_rp_bsr/r1/frr.conf b/tests/topotests/pim_cand_rp_bsr/r1/frr.conf new file mode 100644 index 000000000..d0aa3d529 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r1/frr.conf @@ -0,0 +1,25 @@ +! +hostname r1 +password zebra +log file /tmp/r1-frr.log +! +!debug pim packet +!debug pim bsm +! +ip route 0.0.0.0/0 10.0.0.4 +! +interface r1-eth0 + ip address 10.0.0.1/24 + ip igmp + ip pim +! +interface r1-eth1 + ip address 10.0.1.1/24 + ip igmp + ip pim +! +router pim + bsr candidate-bsr priority 200 source address 10.0.0.1 +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/r2/frr.conf b/tests/topotests/pim_cand_rp_bsr/r2/frr.conf new file mode 100644 index 000000000..741c839f1 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r2/frr.conf @@ -0,0 +1,22 @@ +! +hostname r2 +password zebra +log file /tmp/r2-frr.log +! +ip route 0.0.0.0/0 10.0.0.4 +! +interface r2-eth0 + ip address 10.0.0.2/24 + ip igmp + ip pim +! +interface r2-eth1 + ip address 10.0.2.2/24 + ip igmp + ip pim +! +router pim + bsr candidate-bsr priority 100 source address 10.0.0.2 +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/r3/frr.conf b/tests/topotests/pim_cand_rp_bsr/r3/frr.conf new file mode 100644 index 000000000..bd5c8ce93 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r3/frr.conf @@ -0,0 +1,32 @@ +! +hostname r3 +password zebra +log file /tmp/r3-frr.log +! +!debug pim packet +!debug pim bsm +! +ip route 0.0.0.0/0 10.0.3.4 +ip route 10.0.6.0/24 10.0.3.6 +! +interface r3-eth0 + ip address 10.0.1.3/24 + ip igmp + ip pim +! +interface r3-eth1 + ip address 10.0.3.3/24 + ip igmp + ip pim +! +interface r3-eth2 + ip address 10.0.4.3/24 + ip igmp + ip pim +! +router pim + bsr candidate-rp group 239.0.0.0/16 + bsr candidate-rp priority 10 source address 10.0.3.3 +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/r4/frr.conf b/tests/topotests/pim_cand_rp_bsr/r4/frr.conf new file mode 100644 index 000000000..825b22772 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r4/frr.conf @@ -0,0 +1,37 @@ +! +hostname r4 +password zebra +log file /tmp/r4-frr.log +! +ip route 10.0.1.0/24 10.0.0.1 +ip route 10.0.4.0/24 10.0.3.3 +ip route 10.0.6.0/24 10.0.3.6 +! +interface r4-eth0 + ip address 10.0.2.4/24 + ip igmp + ip pim +! +interface r4-eth1 + ip address 10.0.3.4/24 + ip igmp + ip pim +! +interface r4-eth2 + ip address 10.0.5.4/24 + ip igmp + ip pim +! +interface r4-eth3 + ip address 10.0.0.4/24 + ip igmp + ip pim +! +router pim + bsr candidate-rp group 239.0.0.0/24 + bsr candidate-rp group 239.0.0.0/16 + bsr candidate-rp group 239.0.0.0/8 + bsr candidate-rp priority 20 source address 10.0.3.4 +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/r5/frr.conf b/tests/topotests/pim_cand_rp_bsr/r5/frr.conf new file mode 100644 index 000000000..c934717d0 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r5/frr.conf @@ -0,0 +1,17 @@ +! +hostname r5 +password zebra +log file /tmp/r5-frr.log +! +ip route 0.0.0.0/0 10.0.4.3 +! +interface r5-eth0 + ip address 10.0.4.5/24 + ip igmp + ip pim +! +interface r5-eth1 + ip address 10.0.6.5/24 +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/r6/frr.conf b/tests/topotests/pim_cand_rp_bsr/r6/frr.conf new file mode 100644 index 000000000..fd9d1eb5c --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/r6/frr.conf @@ -0,0 +1,22 @@ +! +hostname r6 +password zebra +log file /tmp/r6-frr.log +! +ip route 0.0.0.0/0 10.0.6.6 +! +interface r6-eth0 + ip address 10.0.5.6/24 + ip igmp + ip pim +! +interface r6-eth1 + ip address 10.0.6.6/24 +! +interface r6-eth2 + ip address 10.0.3.6/24 + ip igmp + ip pim +! +ip forwarding +! diff --git a/tests/topotests/pim_cand_rp_bsr/test_pim_cand_rp_bsr.py b/tests/topotests/pim_cand_rp_bsr/test_pim_cand_rp_bsr.py new file mode 100644 index 000000000..ce7bc9dc5 --- /dev/null +++ b/tests/topotests/pim_cand_rp_bsr/test_pim_cand_rp_bsr.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + +# +# test_pim_cand_rp_bsr.py +# +# Copyright (c) 2024 ATCorp +# Jafar Al-Gharaibeh +# + +import os +import sys +import pytest +import json +from functools import partial + +# pylint: disable=C0413 +# Import topogen and topotest helpers +from lib import topotest +from lib.topogen import Topogen, get_topogen +from lib.topolog import logger +from lib.pim import verify_pim_rp_info +from lib.common_config import step, write_test_header, retry + +from time import sleep + +""" +test_pim_cand_rp_bsr.py: Test candidate RP/BSR functionality +""" + +TOPOLOGY = """ + Candidate RP/BSR functionality + + +---+---+ +---+---+ + | C-BSR | 10.0.0.0/24 | C-BSR | + + R1 + <--------+---------> + R2 | + |elected| .1 | .2 | | + +---+---+ | +---+---+ + .1 | | 10.0.2.0/24 | .2 + | 10.0.1.0/24 | | + .3 | +-----| .4 | .4 + +---+---+ |---->+---+---+ + | C-RP | 10.0.3.0/24 | C-RP | + + R3 + <--------+---------> + R4 | + | prio | .3 | .4 | | + +---+---+ | +---+---+ + .3 | | | .4 + |10.0.4.0/24 | 10.0.5.0/24| + .5 | | .6 | .6 + +---+---+ +---------->+---+---+ + | | | | + + R5 + <------------------> + R6 | + | | .5 .6 | | + +---+---+ 10.0.6.0/24 +---+---+ +""" + +# Save the Current Working Directory to find configuration files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# Required to instantiate the topology builder class. +pytestmark = [pytest.mark.pimd] + + +def build_topo(tgen): + "Build function" + + # Create 6 routers + for rn in range(1, 7): + tgen.add_router("r{}".format(rn)) + + # Create 7 switches and connect routers + sw1 = tgen.add_switch("s1") + sw1.add_link(tgen.gears["r1"]) + sw1.add_link(tgen.gears["r2"]) + + sw = tgen.add_switch("s2") + sw.add_link(tgen.gears["r1"]) + sw.add_link(tgen.gears["r3"]) + + sw = tgen.add_switch("s3") + sw.add_link(tgen.gears["r2"]) + sw.add_link(tgen.gears["r4"]) + + sw3 = tgen.add_switch("s4") + sw3.add_link(tgen.gears["r3"]) + sw3.add_link(tgen.gears["r4"]) + + sw = tgen.add_switch("s5") + sw.add_link(tgen.gears["r3"]) + sw.add_link(tgen.gears["r5"]) + + sw = tgen.add_switch("s6") + sw.add_link(tgen.gears["r4"]) + sw.add_link(tgen.gears["r6"]) + + sw = tgen.add_switch("s7") + sw.add_link(tgen.gears["r5"]) + sw.add_link(tgen.gears["r6"]) + + # make the diagnoal connections + sw1.add_link(tgen.gears["r4"]) + sw3.add_link(tgen.gears["r6"]) + +def setup_module(mod): + logger.info("PIM Candidate RP/BSR:\n {}".format(TOPOLOGY)) + + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + router_list = tgen.routers() + for rname, router in router_list.items(): + logger.info("Loading router %s" % rname) + router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname))) + + # Initialize all routers. + tgen.start_router() + for router in router_list.values(): + if router.has_version("<", "4.0"): + tgen.set_error("unsupported version") + + +def teardown_module(mod): + "Teardown the pytest environment" + tgen = get_topogen() + tgen.stop_topology() + +def test_pim_bsr_election_r1(request): + "Test PIM BSR Election" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r2 = tgen.gears["r2"] + # r1 should be the BSR winner because it has higher priority + expected = { + "bsr":"10.0.0.1", + "priority":200, + "state":"ACCEPT_PREFERRED", + } + + test_func = partial( + topotest.router_json_cmp, r2, "show ip pim bsr json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) + + assertmsg = "r2: r1 was not elected, bsr election mismatch" + assert result is None, assertmsg + +def test_pim_bsr_cand_bsr_r1(request): + "Test PIM BSR candidate BSR" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r2 = tgen.gears["r2"] + + # r2 is a candidate bsr with low priority: elected = False + expected = { + "address": "10.0.0.2", + "priority": 100, + "elected": False + } + test_func = partial( + topotest.router_json_cmp, r2, "show ip pim bsr candidate-bsr json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) + + assertmsg = "r2: candidate bsr mismatch " + assert result is None, assertmsg + +def test_pim_bsr_cand_rp(request): + "Test PIM BSR candidate RP" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r3 = tgen.gears["r3"] + + # r3 is a candidate rp + expected = { + "address":"10.0.3.3", + "priority":10 + } + test_func = partial( + topotest.router_json_cmp, r3, "show ip pim bsr candidate-rp json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) + + assertmsg = "r3: bsr candidate rp mismatch" + assert result is None, assertmsg + + +def test_pim_bsr_rp_info(request): + "Test RP info state" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + # At this point, all nodes, including r5 should have synced the RP state + step("Verify rp-info on r5 from BSR") + result = verify_pim_rp_info(tgen, None, "r5", "239.0.0.0/16", None, "10.0.3.3", + "BSR", False, "ipv4", True, retry_timeout = 90) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + result = verify_pim_rp_info(tgen, None, "r5", "239.0.0.0/8", None, "10.0.3.4", + "BSR", False, "ipv4", True, retry_timeout = 30) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + result = verify_pim_rp_info(tgen, None, "r5", "239.0.0.0/24", None, "10.0.3.4", + "BSR", False, "ipv4", True, retry_timeout = 30) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("Verify rp-info on the BSR node itself r1") + result = verify_pim_rp_info(tgen, None, "r1", "239.0.0.0/16", None, "10.0.3.3", + "BSR", False, "ipv4", True, retry_timeout = 10) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + result = verify_pim_rp_info(tgen, None, "r1", "239.0.0.0/8", None, "10.0.3.4", + "BSR", False, "ipv4", True, retry_timeout = 10) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + result = verify_pim_rp_info(tgen, None, "r1", "239.0.0.0/24", None, "10.0.3.4", + "BSR", False, "ipv4", True, retry_timeout = 10) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + +def test_pim_bsr_election_fallback_r2(request): + "Test PIM BSR Election Backup" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + step("Take r1 out from BSR candidates") + r1 = tgen.gears["r1"] + r1.vtysh_cmd( + """ + configure + router pim + no bsr candidate-bsr priority 200 source address 10.0.0.1 + """) + + step("Verify r1 is no longer a BSR candidate") + expected = {} + + test_func = partial( + topotest.router_json_cmp, r1, "show ip pim bsr candidate-bsr json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=10, wait=1) + + assertmsg = "r1: failed to remove bsr candidate configuration" + assert result is None, assertmsg + + r2 = tgen.gears["r2"] + # We should fall back to r2 as the BSR + expected = { + "bsr":"10.0.0.2", + "priority":100, + "state":"BSR_ELECTED", + } + + step("Verify that we fallback to r2 as the new BSR") + + test_func = partial( + topotest.router_json_cmp, r2, "show ip pim bsr json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=180, wait=1) + + assertmsg = "r2: failed to fallback to r2 as a BSR" + assert result is None, assertmsg + + +def test_pim_bsr_rp_info_fallback(request): + "Test RP info state on r5" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + step("Take r3 out from RP candidates for group 239.0.0.0/16") + r3 = tgen.gears["r3"] + r3.vtysh_cmd( + """ + configure + router pim + no bsr candidate-rp group 239.0.0.0/16 + """) + + step("Verify falling back to r4 as the new RP for 239.0.0.0/16") + + result = verify_pim_rp_info(tgen, None, "r5", "239.0.0.0/16", None, "10.0.3.4", + "BSR", False, "ipv4", True, retry_timeout = 30) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + +def test_memory_leak(): + "Run the memory leak test and report results." + tgen = get_topogen() + if not tgen.is_memleak_enabled(): + pytest.skip("Memory leak test/report is disabled") + + tgen.report_memory_leaks() + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) |