Commit cc8aeb754b for frr
commit cc8aeb754b3b5242998b5f3fcf6cc00ea1477b0d
Author: Nageswara Soma <nsoma@cisco.com>
Date: Tue Oct 6 16:23:59 2026 -0700
tests: verify EVPN-MH state across BGP re-create
Delete and re-add the BGP instance on torm11
twice while its local ESs stay up in zebra.
Check that the ES is no longer local and its
Type-4 route is withdrawn from torm12, then that
the ES-EVI count, the DF roles and the Type-1,
Type-2 and Type-4 routes for that ES and its VNI
are restored on both peers.
Signed-off-by: Nageswara Soma <nsoma@cisco.com>
diff --git a/tests/topotests/bgp_evpn_mh/test_evpn_mh.py b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
index c4cf7f5485..25e72ab15e 100644
--- a/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
+++ b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
@@ -1273,6 +1273,193 @@ def test_evpn_svi_mac_withdraw_on_svi_del():
config_svi(origin, svi_ips.get(origin_name))
+def check_type4_route(dut, esi, originator, expected, self_orig=False):
+ """
+ Return None if the presence of the Type-4 route for esi/originator
+ matches expected, the command output otherwise.
+ """
+ command = "show bgp l2vpn evpn route type 4"
+ if self_orig:
+ command += " self-originate"
+
+ output = dut.vtysh_cmd(command)
+ present = any(esi in line and originator in line for line in output.splitlines())
+ if present == expected:
+ return None
+
+ return output
+
+
+def get_evpn_rd_paths(node, route_type, rd, match):
+ """
+ Return the valid paths of the route_type routes under rd whose prefix
+ contains match.
+ """
+ routes = json.loads(
+ node.vtysh_cmd("show bgp l2vpn evpn route type %d json" % route_type) or "{}"
+ )
+ paths = []
+ for prefix, info in routes.get(rd, {}).items():
+ if not isinstance(info, dict) or match not in prefix:
+ continue
+ for path_list in info.get("paths", []):
+ for path in path_list if isinstance(path_list, list) else [path_list]:
+ if path.get("valid"):
+ paths.append(path)
+
+ return paths
+
+
+def check_es_evpn_routes(tgen, dut, observer, esi, mac, vni):
+ """
+ Return None if the Type-1 and Type-2 routes that dut originates for esi
+ are present on dut and observer, an error string otherwise. The
+ VxLAN devices in this topology use a multicast underlay, so no Type-3
+ routes are exchanged.
+ """
+ es_rd = get_bgp_es(dut, esi).get("rd")
+ vni_rd = json.loads(
+ dut.vtysh_cmd("show bgp l2vpn evpn vni %d json" % vni) or "{}"
+ ).get("rd")
+ if not es_rd or not vni_rd:
+ return "%s ES RD %s or VNI %d RD %s missing" % (dut.name, es_rd, vni, vni_rd)
+
+ # refresh the host MACs so the ES MAC route is originated again
+ ping_anycast_gw(tgen)
+
+ routes = (
+ ("EAD-per-ES", 1, es_rd, "[4294967295]:[%s]" % esi, None),
+ ("EAD-per-EVI", 1, vni_rd, "[0]:[%s]" % esi, None),
+ ("MAC-IP", 2, vni_rd, "[%s]" % mac, esi),
+ )
+ errors = []
+ for node in (dut, observer):
+ for name, route_type, rd, match, path_esi in routes:
+ paths = get_evpn_rd_paths(node, route_type, rd, match)
+ if path_esi:
+ paths = [path for path in paths if path.get("esi") == path_esi]
+ if not paths:
+ errors.append(
+ "%s missing %s route %s under RD %s" % (node.name, name, match, rd)
+ )
+
+ return "\n".join(errors) if errors else None
+
+
+def get_bgp_es(dut, esi):
+ return json.loads(dut.vtysh_cmd("show bgp l2vpn evpn es %s json" % esi) or "{}")
+
+
+def check_bgp_es_local(dut, esi, expected):
+ """
+ Return None if the BGP local state of the ES matches expected.
+ """
+ es = get_bgp_es(dut, esi)
+ local = "local" in es.get("type", [])
+ if local == expected:
+ return None
+
+ return "ES local is %s, expected %s: %s" % (local, expected, es)
+
+
+def check_bgp_instance_absent(dut, instance):
+ output = dut.vtysh_cmd("show running-config bgpd")
+ if instance not in output.splitlines():
+ return None
+
+ return output
+
+
+def check_rebuilt_es(tgen, dut, observer, esi, originator, mac, vni, vni_count):
+ """
+ Return None once the local ES and all its EVPN state is rebuilt on dut
+ and visible on observer, an error string otherwise.
+ """
+ es = get_bgp_es(dut, esi)
+ checks = (
+ ("ES routes", check_es_evpn_routes(tgen, dut, observer, esi, mac, vni)),
+ (
+ "local Type-4",
+ check_type4_route(dut, esi, originator, True, self_orig=True),
+ ),
+ ("peer Type-4", check_type4_route(observer, esi, originator, True)),
+ (
+ "ES-EVI count",
+ (
+ None
+ if es.get("vniCount") == vni_count
+ else "vniCount %s, expected %s: %s"
+ % (es.get("vniCount"), vni_count, es)
+ ),
+ ),
+ ("local DF role", check_df_role(dut, esi, "DF")),
+ ("peer DF role", check_df_role(observer, esi, "nonDF")),
+ )
+ errors = ["%s: %s" % check for check in checks if check[1] is not None]
+ return "\n".join(errors) if errors else None
+
+
+def test_evpn_mh_bgp_instance_delete_readd():
+ """
+ Delete and re-add the BGP instance on torm11 while its local ES stays
+ operational in zebra.
+ 1. Verify the local ES is released and its Type-4 route is withdrawn
+ on torm12 after the instance is deleted.
+ 2. Re-add the instance and verify the ES is rebuilt with the same
+ ES-EVI count, the ES's Type-1, Type-2 and Type-4 routes are
+ exchanged again and the DF election result is unchanged.
+ 3. Repeat once more to exercise an ES that outlived the instance.
+ """
+
+ tgen = get_topogen()
+
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ dut = tgen.gears["torm11"]
+ observer = tgen.gears["torm12"]
+ esi = host_es_map["hostd12"]
+ originator = tor_ips["torm11"]
+ _, mac = compute_host_ip_mac("hostd12")
+ vni = 1000
+ instance = "router bgp 65002"
+
+ test_fn = partial(check_type4_route, observer, esi, originator, True)
+ _, result = topotest.run_and_expect(test_fn, None, count=30, wait=1)
+ assert result is None, result
+
+ test_fn = partial(check_es_evpn_routes, tgen, dut, observer, esi, mac, vni)
+ _, result = topotest.run_and_expect(test_fn, None, count=30, wait=1)
+ assert result is None, result
+
+ vni_count = get_bgp_es(dut, esi).get("vniCount")
+ assert vni_count, "ES %s has no ES-EVIs on %s" % (esi, dut.name)
+
+ for _ in range(2):
+ dut.vtysh_cmd("configure terminal\nno %s" % instance)
+ test_fn = partial(check_bgp_instance_absent, dut, instance)
+ _, result = topotest.run_and_expect(test_fn, None, count=30, wait=1)
+ assert result is None, result
+
+ test_fn = partial(check_bgp_es_local, dut, esi, False)
+ _, result = topotest.run_and_expect(test_fn, None, count=30, wait=1)
+ assert result is None, result
+
+ test_fn = partial(check_type4_route, observer, esi, originator, False)
+ _, result = topotest.run_and_expect(test_fn, None, count=30, wait=1)
+ assert result is None, result
+
+ dut.cmd_raises("vtysh -f %s/torm11/evpn.conf" % CWD)
+
+ test_fn = partial(
+ check_rebuilt_es, tgen, dut, observer, esi, originator, mac, vni, vni_count
+ )
+ _, result = topotest.run_and_expect(test_fn, None, count=60, wait=1)
+ assert result is None, result
+
+ assert dut.check_router_running() == "", "torm11 daemons are not running"
+
+
if __name__ == "__main__":
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))