summaryrefslogtreecommitdiffstats
path: root/tests/topotests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/topotests')
-rw-r--r--tests/topotests/all_protocol_startup/r1/ip_nht.ref24
-rw-r--r--tests/topotests/all_protocol_startup/r1/ipv6_nht.ref6
-rw-r--r--tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py35
-rwxr-xr-xtests/topotests/conftest.py38
-rw-r--r--tests/topotests/lib/pim.py2
-rw-r--r--tests/topotests/lib/topotest.py18
-rw-r--r--tests/topotests/mgmt_oper/r1/frr-yanglib.conf10
-rw-r--r--tests/topotests/mgmt_oper/test_yanglib.py45
-rw-r--r--tests/topotests/munet/cli.py56
-rw-r--r--tests/topotests/munet/native.py4
-rw-r--r--tests/topotests/munet/testing/util.py26
-rwxr-xr-xtests/topotests/pim_cand_rp_bsr/__init__.py0
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r1/frr.conf25
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r2/frr.conf22
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r3/frr.conf32
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r4/frr.conf37
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r5/frr.conf17
-rw-r--r--tests/topotests/pim_cand_rp_bsr/r6/frr.conf22
-rw-r--r--tests/topotests/pim_cand_rp_bsr/test_pim_cand_rp_bsr.py324
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))