Commit ef57cbb740 for frr
commit ef57cbb7407bf41cc402a052717ddc68028a6479
Author: Harini Nattamai Subramanian <hnattamaisub@nvidia.com>
Date: Tue Sep 22 23:48:07 2026 -0700
tests: expand EVPN Type-5 skip topotest coverage
Tests:
1. test_skip_evpn_imported_survives_bgpd_restart
Verifies suppression survives an FRR/BGP daemon restart.
2. test_skip_evpn_imported_survives_bgp_session_clear
Verifies routes reconverge correctly after clearing BGP sessions.
3. test_skip_evpn_imported_ipv6_is_independent_from_ipv4
Verifies IPv4 and IPv6 suppression controls operate independently.
4. test_skip_evpn_imported_preserves_local_type5_export
Verifies locally originated routes remain exportable.
5. test_skip_evpn_imported_scale_and_repeated_toggle
Verifies large-scale Type-5 handling across repeated enable/disable cycles.
6. test_skip_evpn_imported_survives_route_refresh
Verifies suppression remains effective after a BGP route refresh.
7. test_skip_evpn_imported_preserves_default_route_export
Verifies default-route origination remains unaffected.
8. test_skip_evpn_imported_prefix_move_between_vrfs
Verifies stale Type-5 state is removed when a prefix moves between VRFs.
9. test_skip_evpn_imported_coexists_with_route_map
Verifies route-map filtering and EVPN ancestry suppression work together.
10. test_skip_evpn_imported_withdrawal_is_vrf_scoped
Verifies withdrawal is limited to the affected tenant vrf.
11. test_skip_evpn_imported_multiple_source_vrfs_to_purple
Verifies EVPN-derived routes from multiple source VRFs are suppressed.
12. test_skip_evpn_imported_suppresses_after_initial_vrf_import
Verifies suppression after an existing VRF import is added.
13. test_skip_evpn_imported_selectively_withdraws_evpn_derived_paths
Verifies only EVPN-derived paths are withdrawn.
14. test_skip_evpn_imported_cleans_up_after_vrf_import_removal
Verifies stale Type-5 routes are removed after VRF import removal.
15. test_skip_evpn_imported_preserves_mixed_local_and_evpn_paths
Verifies local paths remain while EVPN-derived paths are withdrawn.
16. test_skip_evpn_imported_ebgp_runtime_toggle_and_withdrawal
Verifies runtime toggling and Type-5 withdrawal cleanup in an eBGP topology.
17. test_skip_evpn_imported_ebgp_clear_and_uplink_failover
Verifies BGP clear and single-uplink failure recovery with eBGP.
18. test_evpn_mh_skip_evpn_imported_smoke
Verifies EVPN-MH peers, ESI state, and dataplane behavior remain stable.
Signed-off-by: Harini Nattamai Subramanian <hnattamaisub@nvidia.com>
diff --git a/tests/topotests/bgp_evpn_mh/test_evpn_mh.py b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
index 25e72ab15e..525b32cda5 100644
--- a/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
+++ b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
@@ -1460,6 +1460,96 @@ def test_evpn_mh_bgp_instance_delete_readd():
assert dut.check_router_running() == "", "torm11 daemons are not running"
+def test_evpn_mh_skip_evpn_imported_smoke():
+ """Verify skip-evpn-imported coexists with EVPN-MH forwarding state."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ # This topology is L2 EVPN-MH and has no tenant L3VNI. Use the default
+ # BGP EVPN context for the CLI coexistence check, while the MH assertions
+ # validate the existing ES control plane and dataplane behavior.
+ tors = (tgen.gears["torm11"], tgen.gears["torm12"])
+ asns = {"torm11": 65002, "torm12": 65003}
+ peers = {"torm11": ("leaf1", "leaf2"), "torm12": ("leaf1", "leaf2")}
+ command = "advertise ipv4 unicast skip-evpn-imported"
+
+ def configure_skip(node, enabled):
+ state = command if enabled else "no " + command
+ node.vtysh_cmd(
+ "configure terminal\n"
+ "router bgp {asn}\n"
+ " address-family l2vpn evpn\n"
+ " {state}\n"
+ " exit-address-family\n"
+ "end".format(asn=asns[node.name], state=state)
+ )
+
+ try:
+ for tor in tors:
+ configure_skip(tor, True)
+ running = tor.vtysh_cmd("show running-config bgpd")
+ assert (
+ command in running
+ ), "{0}: expected {1} in running configuration\n{2}".format(
+ tor.name, command, running
+ )
+
+ for tor in tors:
+
+ def evpn_peers_established(tor=tor):
+ output = tor.vtysh_cmd("show bgp l2vpn evpn summary")
+ bad_states = ("Active", "Connect", "Idle", "OpenSent", "OpenConfirm")
+ return (
+ None
+ if all(peer in output for peer in peers[tor.name])
+ and not any(state in output for state in bad_states)
+ else output
+ )
+
+ _, result = topotest.run_and_expect(
+ evpn_peers_established, None, count=20, wait=3
+ )
+ assert (
+ result is None
+ ), "{0}: expected EVPN peers {1} established\n{2}".format(
+ tor.name, peers[tor.name], result
+ )
+
+ def mh_state_present(tor=tor):
+ output = tor.vtysh_cmd("show evpn es detail")
+ return None if "ESI" in output else output
+
+ _, result = topotest.run_and_expect(
+ mh_state_present, None, count=20, wait=3
+ )
+ assert result is None, "{0}: expected ESI state present\n{1}".format(
+ tor.name, result
+ )
+
+ remote_esi = host_es_map["hostd21"]
+ ping_anycast_gw(tgen)
+ for tor in tors:
+ # Remote ES control-plane state is verified in bgpd, using the
+ # same checker as the existing MH tests (including its VTEP list).
+ test_fn = partial(check_one_es, tor, remote_esi, [])
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assert result is None, (
+ "{0}: expected remote EVPN-MH ESI {1} and its VTEPs; "
+ "actual={2}\nBGP ES state:\n{3}\nZebra ES state:\n{4}".format(
+ tor.name,
+ remote_esi,
+ result,
+ tor.vtysh_cmd("show bgp l2vpn evpn es detail json"),
+ tor.vtysh_cmd("show evpn es detail"),
+ )
+ )
+ finally:
+ for tor in tors:
+ configure_skip(tor, False)
+
+
if __name__ == "__main__":
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))
diff --git a/tests/topotests/bgp_evpn_three_tier_clos_topo1/README.md b/tests/topotests/bgp_evpn_three_tier_clos_topo1/README.md
index c4f388c43f..0043b6250c 100644
--- a/tests/topotests/bgp_evpn_three_tier_clos_topo1/README.md
+++ b/tests/topotests/bgp_evpn_three_tier_clos_topo1/README.md
@@ -317,7 +317,22 @@ All underlay links use IPv6 /126 subnets from fd00:10:254::/32
## Test Cases
-**Note:** All tests are parametrized to run with both IPv4 and IPv6 underlay configurations.
+### `skip-evpn-imported` eBGP Feature Tests
+Both tests run for the IPv4 and IPv6 underlays, and use the address family
+of the underlay for the advertise option, the Type-5 routes and the eBGP
+peer checks.
+
+- **`test_skip_evpn_imported_ebgp_runtime_toggle_and_withdrawal`** -
+ On `tor-21` and `tor-22`, toggles
+ `advertise <afi> unicast skip-evpn-imported` in `vrf1`. It verifies a local
+ Type-5 route (`203.0.250.0/24` or `2001:db8:250:21::/64`) is present,
+ enables the option on both ToRs, then withdraws the route and confirms its
+ local Type-5 origin is removed.
+
+- **`test_skip_evpn_imported_ebgp_clear_and_uplink_failover`** -
+ Enables suppression on both ToRs and checks eBGP peer state and each
+ ToR's local Type-5 export of its `vrf1` static route. It repeats the checks
+ after taking `tor-21`'s `swp1` down, clearing BGP, and restoring the uplink.
### Protocol Convergence Tests
1. **test_bgp_summary_neighbor_state()** - Verify BGP sessions are established (ipv4Unicast/ipv6Unicast based on underlay)
diff --git a/tests/topotests/bgp_evpn_three_tier_clos_topo1/test_bgp_evpn_v4_v6_vtep.py b/tests/topotests/bgp_evpn_three_tier_clos_topo1/test_bgp_evpn_v4_v6_vtep.py
index f4bf085ab1..5342360f54 100644
--- a/tests/topotests/bgp_evpn_three_tier_clos_topo1/test_bgp_evpn_v4_v6_vtep.py
+++ b/tests/topotests/bgp_evpn_three_tier_clos_topo1/test_bgp_evpn_v4_v6_vtep.py
@@ -1619,6 +1619,194 @@ def cleanup_route_leak_common(tor21, tor22, bordertor11, ip_version, test_route)
logger.info("Common configuration removed successfully")
+def _set_ebgp_skip(router, asn, address_family, enabled):
+ command = "advertise {0} unicast skip-evpn-imported".format(address_family)
+ if not enabled:
+ command = "no " + command
+ router.vtysh_multicmd(
+ """
+ configure terminal
+ router bgp {asn} vrf vrf1
+ address-family l2vpn evpn
+ {command}
+ exit-address-family
+ exit
+ """.format(
+ asn=asn, command=command
+ )
+ )
+
+
+def _wait_ebgp_established(router, address_family, require_all=True):
+ summary_key = "ipv4Unicast" if address_family == "ipv4" else "ipv6Unicast"
+
+ def _check():
+ summary = router.vtysh_cmd("show bgp summary json", isjson=True)
+ peers = summary.get(summary_key, {}).get("peers", {})
+ established = [
+ peer for peer in peers.values() if peer.get("state") == "Established"
+ ]
+ if require_all:
+ return bool(peers) and len(established) == len(peers)
+ return bool(established)
+
+ _, result = topotest.run_and_expect(_check, True, count=60, wait=1)
+ assert result, "{0}: {1} expected {2} eBGP peers Established\n{3}\n{4}".format(
+ router.name,
+ address_family,
+ "all" if require_all else "at least one",
+ router.vtysh_cmd("show bgp summary json"),
+ router.run("ip -brief link show"),
+ )
+
+
+def test_skip_evpn_imported_ebgp_runtime_toggle_and_withdrawal(tgen_and_ip_version):
+ """Verify eBGP runtime toggling and Type-5 withdrawal cleanup."""
+
+ tgen, ip_version = tgen_and_ip_version
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ tor21 = tgen.gears["tor-21"]
+ tor22 = tgen.gears["tor-22"]
+ address_family = ip_version
+ if address_family == "ipv4":
+ prefix = "203.0.250.0/24"
+ route_command = "ip route {0} blackhole".format(prefix)
+ else:
+ prefix = "2001:db8:250:21::/64"
+ route_command = "ipv6 route {0} blackhole".format(prefix)
+ address = prefix.split("/", 1)[0]
+
+ try:
+ _set_ebgp_skip(tor21, 650030, address_family, False)
+ _set_ebgp_skip(tor22, 650031, address_family, False)
+ tor21.vtysh_multicmd(
+ "configure terminal\nvrf vrf1\n{0}\nexit\n".format(route_command)
+ )
+
+ def _origin_present():
+ return address in tor21.vtysh_cmd(
+ "show bgp l2vpn evpn route self-originate"
+ )
+
+ _, result = topotest.run_and_expect(_origin_present, True, count=60, wait=1)
+ assert (
+ result
+ ), "tor-21 vrf1 {0}: expected local Type-5 present, actual absent\n{1}".format(
+ prefix, tor21.vtysh_cmd("show bgp l2vpn evpn route self-originate")
+ )
+
+ _set_ebgp_skip(tor21, 650030, address_family, True)
+ _set_ebgp_skip(tor22, 650031, address_family, True)
+ marker = "advertise {0} unicast skip-evpn-imported".format(address_family)
+ for router in (tor21, tor22):
+ running = router.vtysh_cmd("show running-config bgpd")
+ assert (
+ marker in running
+ ), "{0} vrf1: expected {1} in running configuration\n{2}".format(
+ router.name, marker, running
+ )
+
+ tor21.vtysh_multicmd(
+ "configure terminal\nvrf vrf1\nno {0}\nexit\n".format(route_command)
+ )
+
+ def _origin_withdrawn():
+ return address not in tor21.vtysh_cmd(
+ "show bgp l2vpn evpn route self-originate"
+ )
+
+ _, result = topotest.run_and_expect(_origin_withdrawn, True, count=60, wait=1)
+ assert (
+ result
+ ), "tor-21 vrf1 {0}: expected local Type-5 absent, actual present\n{1}".format(
+ prefix, tor21.vtysh_cmd("show bgp l2vpn evpn route self-originate")
+ )
+ finally:
+ tor21.vtysh_multicmd(
+ "configure terminal\nvrf vrf1\nno {0}\nexit\n".format(route_command)
+ )
+ _set_ebgp_skip(tor21, 650030, address_family, False)
+ _set_ebgp_skip(tor22, 650031, address_family, False)
+
+
+def test_skip_evpn_imported_ebgp_clear_and_uplink_failover(tgen_and_ip_version):
+ """Verify export and skip state across eBGP failover and clear."""
+
+ tgen, ip_version = tgen_and_ip_version
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ tor21 = tgen.gears["tor-21"]
+ tor22 = tgen.gears["tor-22"]
+ address_family = ip_version
+ if address_family == "ipv4":
+ expected_routes = {
+ "tor-21": "203.0.113.0/24",
+ "tor-22": "203.0.117.0/24",
+ }
+ else:
+ expected_routes = {
+ "tor-21": "2001:db8:72:21::/64",
+ "tor-22": "2001:db8:72:22::/64",
+ }
+
+ def check_config_and_local_exports():
+ marker = "advertise {0} unicast skip-evpn-imported".format(address_family)
+ for name, route in expected_routes.items():
+ router = tgen.gears[name]
+ running = router.vtysh_cmd("show running-config bgpd")
+ assert (
+ marker in running
+ ), "{0} vrf1: expected {1} in running configuration\n{2}".format(
+ name, marker, running
+ )
+
+ def local_export_present(router=router, route=route):
+ return route.split("/", 1)[0] in router.vtysh_cmd(
+ "show bgp l2vpn evpn route self-originate"
+ )
+
+ _, result = topotest.run_and_expect(
+ local_export_present, True, count=60, wait=1
+ )
+ assert result, (
+ "{0} vrf1 {1}: expected local Type-5 present, actual absent\n{2}"
+ "\nBGP summary:\n{3}\nLink state:\n{4}".format(
+ name,
+ route,
+ router.vtysh_cmd("show bgp l2vpn evpn route self-originate"),
+ router.vtysh_cmd("show bgp summary json"),
+ router.run("ip -brief link show"),
+ )
+ )
+
+ try:
+ _set_ebgp_skip(tor21, 650030, address_family, True)
+ _set_ebgp_skip(tor22, 650031, address_family, True)
+ _wait_ebgp_established(tor21, address_family)
+ _wait_ebgp_established(tor22, address_family)
+ check_config_and_local_exports()
+
+ tor21.run("ip link set dev swp1 down")
+ _wait_ebgp_established(tor21, address_family, require_all=False)
+ check_config_and_local_exports()
+
+ tor21.vtysh_cmd("clear bgp *")
+ _wait_ebgp_established(tor21, address_family, require_all=False)
+ check_config_and_local_exports()
+
+ tor21.run("ip link set dev swp1 up")
+ _wait_ebgp_established(tor21, address_family)
+ _wait_ebgp_established(tor22, address_family)
+ check_config_and_local_exports()
+ finally:
+ tor21.run("ip link set dev swp1 up 2>/dev/null || true")
+ _set_ebgp_skip(tor21, 650030, address_family, False)
+ _set_ebgp_skip(tor22, 650031, address_family, False)
+
+
def test_bgp_summary_neighbor_state(tgen_and_ip_version):
"""
Verify BGP neighbor states are Established based on underlay IP version
diff --git a/tests/topotests/bgp_evpn_type5_vrf_leak_stale/README.md b/tests/topotests/bgp_evpn_type5_vrf_leak_stale/README.md
new file mode 100644
index 0000000000..cf0e376443
--- /dev/null
+++ b/tests/topotests/bgp_evpn_type5_vrf_leak_stale/README.md
@@ -0,0 +1,294 @@
+# EVPN Type-5 VRF Leak and Stale Route Tests
+
+## Overview
+
+This topotest reproduces stale EVPN Type-5 routes created by inter-VRF route
+leaking and checks the behavior of `advertise ipv4 unicast skip-evpn-imported`.
+It verifies that routes learned from EVPN can still be imported and used
+locally, while preventing those EVPN-derived paths from being advertised back
+into EVPN. It also checks that locally originated paths continue to be
+exported, and that withdrawals and configuration changes remove stale paths.
+
+The original regression scenario models a route chain in which an imported
+route is leaked between VRFs and re-exported by two peer VTEPs. If the original
+route is withdrawn, a leaked copy can otherwise keep circulating and remain
+visible after its origin is gone.
+
+## Topology
+
+```text
+ EVPN Type-5
+ +-------------------+
+ | |
+ +-----+-----+ +-----+-----+
+ | r1 | | r2 |
+ | origin PE | | PE/VTEP |
+ +-----+-----+ +-----+-----+
+ | |
+ +---------+---------+
+ |
+ +----+----+
+ | s1 |
+ +----+----+
+ |
+ +----+----+
+ | r3 |
+ | PE/VTEP|
+ +---------+
+```
+
+`r1`, `r2`, and `r3` share a single test switch and establish BGP EVPN
+sessions. Each router has a `GREEN` VRF (VNI 101). `r2` and `r3` also have
+`BLUE` (VNI 102) and `PURPLE` (VNI 103), which are used to exercise multi-step
+and multi-source VRF imports. The test creates the VRFs, bridges, and VXLAN
+interfaces during setup and loads each router's BGP configuration from its
+`r1/`, `r2/`, or `r3/` directory.
+
+## VTEP IP Mapping
+
+| Router | EVPN/VTEP address | BGP AS | VRFs and VNIs |
+| --- | --- | --- | --- |
+| `r1` | `192.168.100.1` | `65000` | `GREEN` / `101` |
+| `r2` | `192.168.100.2` | `65000` | `GREEN` / `101`, `BLUE` / `102`, `PURPLE` / `103` |
+| `r3` | `192.168.100.3` | `65000` | `GREEN` / `101`, `BLUE` / `102`, `PURPLE` / `103` |
+
+These addresses are configured on each router's `*-eth0` interface and are
+used as the EVPN peer and VXLAN local VTEP addresses. Tenant BGP instances use
+router IDs from the corresponding VNI subnet, such as `192.168.102.2` for
+`r2`'s `BLUE` VRF.
+
+## Configuration Highlights
+
+- All three routers use AS `65000` and form EVPN iBGP sessions over the shared
+ `192.168.100.0/24` segment.
+- The base BGP instance activates the fabric peer group under `l2vpn evpn` and
+ advertises all configured VNIs.
+- `r1` originates Type-5 routes from static routes in `GREEN` and advertises
+ IPv4 unicast into EVPN. It is the origin PE in the stale-route scenario.
+- `r2` and `r3` import routes between `GREEN`, `BLUE`, and `PURPLE`. Their
+ tenant BGP EVPN address families use
+ `advertise ipv4 unicast skip-evpn-imported` to prevent re-export of
+ EVPN-derived paths while preserving local route leaking.
+- The tests add and remove routes and change VRF import or advertisement
+ configuration at runtime to check withdrawal, re-advertisement, and cleanup.
+- The topology creates one VRF bridge and VXLAN interface per VNI, with VXLAN
+ learning disabled.
+
+The core EVPN configuration on a tenant VRF is:
+
+```text
+address-family l2vpn evpn
+ advertise ipv4 unicast skip-evpn-imported
+exit-address-family
+```
+
+## Reload-based Configuration Tests
+
+Four testcases exercise `frr-reload.py` on `r2` using its existing
+`/etc/frr/frr.conf`; they do not edit that file dynamically:
+
+- `test_skip_evpn_imported_survives_bgpd_restart` restarts `bgpd` on `r2`,
+ then runs `frr-reload.py --reload /etc/frr/frr.conf` to restore the unified
+ configuration. It verifies BGP reconvergence, VRF route recovery, and that
+ `skip-evpn-imported` still suppresses Type-5 re-export.
+
+- `test_skip_evpn_imported_survives_bgp_session_clear` clears BGP sessions on
+ `r2`, reloads its existing config, then verifies BGP route recovery and no
+ additional Type-5 re-export.
+
+- `test_skip_evpn_imported_survives_route_refresh` reloads `r2`'s existing
+ config before a soft inbound refresh, then verifies imported VRF routes and
+ Type-5 suppression remain correct.
+
+- `test_evpn_type5_vrf_leak_stale_after_origin_withdraw` reloads `r2`'s
+ existing config during the first of three origin cycles, then verifies the
+ origin withdrawal removes all leaked and advertised copies.
+
+The prefix-move and route-map testcases change runtime configuration with
+`vtysh`/the shared route API; they do not use `frr-reload.py`.
+
+## Main Regression Flow
+
+1. `r1` originates `192.168.50.1/32` as a static route in `GREEN` and exports
+ it as an EVPN Type-5 route.
+2. `r2` and `r3` import the route into `GREEN` and leak it between `GREEN`,
+ `BLUE`, and `PURPLE`. The leaked paths are eligible for EVPN re-export.
+3. The test withdraws the original route from `r1`.
+4. It checks that the withdrawn route disappears from the affected VRFs and
+ EVPN tables, and that `r1` does not learn the route back from a peer.
+
+With `skip-evpn-imported` enabled, the peers must retain the imported route for
+local VRF routing while suppressing its EVPN-derived path from re-export.
+Locally originated Type-5 routes remain exportable.
+
+## Test Cases
+
+### Core functional checks and verification points
+
+In this topology, `r2` is the primary DUT under test, `r1` is the route
+origin/remote PE, and `r3` is the second PE. The tests verify control-plane
+state on the DUT and, where applicable, confirm the resulting EVPN Type-5
+advertisement or withdrawal on remote routers. “All routers” means `r1`,
+`r2`, and `r3`.
+
+- **`test_normal_vrf_leak_exports_with_suppress_imported_from_evpn`**
+ - Local: Adds a normal static route in `r2` `BLUE`, verifies the leak into
+ `r2` `GREEN`, and verifies withdrawal from both VRFs after cleanup.
+ - Propagation: Verifies the leaked route reaches `r1` `GREEN` and is
+ withdrawn there.
+
+- **`test_direct_evpn_import_is_not_reexported_without_skip`**
+ - Local: Disables `skip-evpn-imported` in `GREEN` on `r2` and `r3`, then
+ verifies both import the route originated on `r1`.
+ - Propagation: Verifies the Type-5 count stays exactly one on all routers,
+ so direct imports do not create additional Type-5 advertisements.
+
+- **`test_skip_evpn_imported_preserves_local_type5_export`**
+ - Local: On `r2`, verifies the `BLUE` option, local BGP route, Zebra RIB
+ entry, and kernel FIB entry.
+ - Peer-side: Verifies EVPN peers on all routers and BLUE L3VNI state on
+ `r2` and `r3`.
+ - Propagation: Verifies the locally originated Type-5 remains present on
+ all routers.
+
+- **`test_no_skip_evpn_imported_reexports_and_reenabling_cleans_up`**
+ - Local: Verifies option configuration in every tenant VRF on `r2` and
+ `r3` as it is disabled and restored.
+ - Propagation: Verifies the Type-5 count on all routers changes from one
+ to at least two when disabled, then returns to one when re-enabled.
+
+- **`test_skip_evpn_imported_ipv6_is_independent_from_ipv4`**
+ - Local: On `r2` and `r3`, verifies disabling IPv6 suppression leaves
+ IPv4 suppression enabled.
+ - Propagation: No route propagation is asserted; this case verifies
+ address-family configuration independence on both PEs.
+
+- **`test_skip_evpn_imported_preserves_default_route_export`**
+ - Local: Installs a default route in `r2` `BLUE` and verifies it in the
+ local BGP table.
+ - Propagation: Verifies the default Type-5 route is present on `r1`, `r2`,
+ and `r3`.
+
+- **`test_skip_evpn_imported_coexists_with_route_map`**
+ - Local: On `r2`, verifies route-map and skip configuration independently
+ across tenant VRFs; applies route-map changes through `vtysh`.
+ - Propagation: On `r1`, verifies allowed/denied route filtering and Type-5
+ counts as the route-map and skip option are independently removed.
+
+The remaining testcases are described below with their route/configuration
+changes and the resulting DUT or peer checks.
+
+### Baseline route behavior
+
+- `test_normal_vrf_leak_exports_with_suppress_imported_from_evpn` verifies
+ that a locally originated static route in `r2` `BLUE` leaks into `r2`
+ `GREEN` and is exported to `r1` `GREEN`; it then verifies withdrawal from
+ both routers. This testcase does not independently assert suppression of an
+ EVPN-imported route.
+
+- `test_direct_evpn_import_is_not_reexported_without_skip` verifies the
+ baseline behavior with `skip-evpn-imported` disabled in `GREEN` on `r2` and
+ `r3`: both peers import the route from `r1`, while the Type-5 count remains
+ exactly one on all routers, so neither peer adds a re-exported copy.
+
+- `test_evpn_type5_vrf_leak_stale_after_origin_withdraw` runs the original
+ stale-route regression flow for three origin/withdrawal cycles. It checks
+ route presence across `GREEN`, `BLUE`, and `PURPLE` before withdrawal, then
+ verifies the route and Type-5 copies disappear from all routers. During the
+ first cycle it also reloads `r2`'s existing configuration.
+
+### Configuration and reconvergence
+
+- `test_skip_evpn_imported_survives_bgpd_restart` checks suppression after a
+ `bgpd` restart.
+
+- `test_skip_evpn_imported_survives_bgp_session_clear` checks route
+ reconvergence after clearing BGP sessions.
+
+- `test_skip_evpn_imported_survives_route_refresh` checks that a route refresh
+ does not re-advertise suppressed paths.
+
+- `test_skip_evpn_imported_scale_and_repeated_toggle` exercises a larger set
+ of 1,000 Type-5 prefixes originated in `r1` `GREEN`. It verifies one copy
+ per prefix on all routers, then disables and re-enables the option on `r2`
+ and `r3` `BLUE` for two cycles, checking for additional copies and their
+ cleanup after each toggle.
+
+- `test_no_skip_evpn_imported_reexports_and_reenabling_cleans_up` checks that
+ disabling suppression permits re-export and that enabling it again withdraws
+ the EVPN-derived paths.
+
+- `test_skip_evpn_imported_ipv6_is_independent_from_ipv4` verifies that the
+ IPv6 option can be disabled on `r2` and `r3` `BLUE` while IPv4 suppression
+ remains configured; it checks configuration independence rather than route
+ propagation.
+
+### Route selection and export
+
+- `test_skip_evpn_imported_preserves_local_type5_export` checks that locally
+ originated Type-5 routes remain exportable.
+
+- `test_skip_evpn_imported_preserves_default_route_export` checks that default
+ route origination remains unaffected.
+
+- `test_skip_evpn_imported_coexists_with_route_map` checks the interaction
+ between EVPN ancestry suppression and route-map filtering.
+
+- `test_skip_evpn_imported_selectively_withdraws_evpn_derived_paths` checks
+ that enabling suppression on the peer tenant VRFs reduces the Type-5 copies
+ of an EVPN-derived prefix, while a separate locally originated prefix
+ remains exported and installed in `r2` `BLUE`.
+
+- `test_skip_evpn_imported_preserves_mixed_local_and_evpn_paths` checks that
+ when the same prefix has an EVPN origin in `r1` `GREEN` and a local route in
+ `r2` `BLUE`, enabling suppression reduces its Type-5 RD set while preserving
+ the valid local BGP path in `r2` `BLUE`.
+
+### VRF import and withdrawal cleanup
+
+- `test_skip_evpn_imported_prefix_move_between_vrfs` checks stale Type-5
+ cleanup when a prefix is withdrawn from its original `r1` `GREEN` origin
+ and originated instead in `r2` `BLUE`. It verifies the old origin's RDs are
+ gone and the new route propagates through the tenant VRFs and to `r1`.
+
+- `test_skip_evpn_imported_withdrawal_is_vrf_scoped` checks that withdrawal
+ after removing `GREEN` and `PURPLE` imports into `BLUE` on both peers removes
+ the route from `BLUE` but retains it in `PURPLE`; it also checks that some,
+ but not all, Type-5 RDs are withdrawn.
+
+- `test_skip_evpn_imported_multiple_source_vrfs_to_purple` checks suppression
+ for routes originated separately in `r1` `GREEN` and `r2` `BLUE` and imported
+ into `PURPLE`. It verifies both routes arrive there and that the original
+ and legitimate local-leak Type-5 paths remain on all routers.
+
+- `test_skip_evpn_imported_suppresses_after_initial_vrf_import` checks
+ suppression when the route first arrives while `PURPLE` cannot import from
+ `GREEN`, then the `GREEN` import is added. It verifies the route appears in
+ `PURPLE` on both peers while the Type-5 count stays at one.
+
+- `test_skip_evpn_imported_cleans_up_after_vrf_import_removal` checks stale
+ cleanup after removing the `GREEN` import into `BLUE` on both peers. It
+ verifies the route disappears from `BLUE` and the Type-5 count returns to
+ one.
+
+`test_memory_leak` runs the memory-leak check for the module.
+
+## Running the Test
+
+Run the full module from the FRR source tree:
+
+```bash
+sudo pytest -s -v \
+ tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py
+```
+
+Run an individual testcase by adding `-k` with its name, for example:
+
+```bash
+sudo pytest -s -v \
+ tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py \
+ -k test_skip_evpn_imported_survives_bgpd_restart
+```
+
+The module requires a Linux kernel version of 4.18 or newer. Pytest skips the
+module during setup when the running kernel is older.
diff --git a/tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py b/tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py
index 5febf1cbb6..1dc4b612ab 100644
--- a/tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py
+++ b/tests/topotests/bgp_evpn_type5_vrf_leak_stale/test_bgp_evpn_type5_vrf_leak_stale.py
@@ -10,11 +10,24 @@ Reproducer for stale EVPN type-5 re-export after inter-VRF leaking.
Topology:
- r1 -----+
- |
- r2 -----+ s1
- |
- r3 -----+
+ EVPN Type-5
+ +-------------------+
+ | |
+ +-----+-----+ +-----+-----+
+ | r1 | | r2 |
+ | origin PE | | PE/VTEP |
+ +-----+-----+ +-----+-----+
+ | |
+ +---------+---------+
+ |
+ +----+----+
+ | s1 |
+ +----+----+
+ |
+ +----+----+
+ | r3 |
+ | PE/VTEP|
+ +---------+
r1 injects 192.168.50.1/32 as a static route in VRF GREEN and exports it as an
EVPN type-5 route. r2 and r3 both import that EVPN route into GREEN, use
@@ -39,7 +52,9 @@ route can still be leaked into r2 GREEN and exported to r1 GREEN as Type-5.
import json
import os
import platform
+import re
import sys
+import time
import pytest
@@ -48,7 +63,19 @@ sys.path.append(os.path.join(CWD, "../"))
# pylint: disable=C0413
from lib import topotest
-from lib.common_config import step
+from lib.common_config import (
+ create_static_routes,
+ create_vrf_cfg,
+ kill_router_daemons,
+ start_router_daemons,
+)
+from lib.common_check import iproute2_check_path_selection
+from lib.bgp import create_router_bgp, verify_bgp_convergence_from_running_config
+from lib.evpn import (
+ evpn_type5_prefix_count,
+ evpn_type5_route_distinguishers,
+ evpn_verify_vrf_rib_route,
+)
from lib.topogen import Topogen, get_topogen
from lib.topolog import logger
@@ -58,11 +85,32 @@ PREFIX = "192.168.50.1/32"
NORMAL_LEAK_PREFIX = "192.168.60.1/32"
DIRECT_IMPORT_PREFIX = "192.168.70.1/32"
TOGGLE_PREFIX = "192.168.80.1/32"
+RESTART_PREFIX = "192.168.90.1/32"
+SESSION_CLEAR_PREFIX = "192.168.103.1/32"
+L3VNI_PREFIX = "192.168.91.1/32"
+SELECTIVE_EVPN_PREFIX = "192.168.92.1/32"
+SELECTIVE_LOCAL_PREFIX = "192.168.93.1/32"
+MIXED_PATH_PREFIX = "192.168.94.1/32"
+VRF_IMPORT_PREFIX = "192.168.95.1/32"
+SHARED_RD_PREFIX = "192.168.96.1/32"
+L3_HOST_WITHDRAW_PREFIX = "192.168.97.1/32"
+PRECONFIGURED_SKIP_PREFIX = "192.168.99.1/32"
+MULTI_SOURCE_GREEN_PREFIX = "192.168.100.1/32"
+MULTI_SOURCE_BLUE_PREFIX = "192.168.101.1/32"
+INITIAL_IMPORT_PREFIX = "192.168.102.1/32"
PE_VRFS = ("GREEN", "BLUE", "PURPLE")
+BLUE_L3VNI = 102
+PURPLE_L3VNI = 103
+# Fabric sessions live in the default instance; the tenant VRFs have no peers.
+FABRIC_PEERS = {
+ "r1": ("192.168.100.2", "192.168.100.3"),
+ "r2": ("192.168.100.1", "192.168.100.3"),
+ "r3": ("192.168.100.1", "192.168.100.2"),
+}
def build_topo(tgen):
- "Build function"
+ "Build three routers connected through a shared test switch."
for router in ("r1", "r2", "r3"):
tgen.add_router(router)
@@ -72,58 +120,69 @@ def build_topo(tgen):
switch.add_link(tgen.gears[router])
-def _run_cmds(router, commands):
+def _configure_l3vni_interfaces(router, rname, vrf, vni, local_vtep):
+ "Create the dummy, bridge, and VXLAN interfaces for one L3VNI."
+ commands = [
+ "ip link add loop{0} type dummy".format(vni),
+ "ip link set dev loop{0} master {1}".format(vni, vrf),
+ "ip link set dev loop{0} up".format(vni),
+ "ip link add name br{0} up type bridge stp_state 0".format(vni),
+ "ip link set dev br{0} master {1}".format(vni, vrf),
+ "ip link set dev br{0} up".format(vni),
+ (
+ "ip link add name vxlan{0} type vxlan id {0} dstport 4789 "
+ "dev {1}-eth0 local {2} nolearning"
+ ).format(vni, rname, local_vtep),
+ "ip link set dev vxlan{0} master br{0}".format(vni),
+ (
+ "ip link set dev vxlan{0} up type bridge_slave "
+ "learning off flood off mcast_flood off"
+ ).format(vni),
+ ]
for command in commands:
logger.info("%s: %s", router.name, command)
router.cmd_raises(command)
-def _add_vrf_vni(router, rname, vrf, table, vni, local_vtep):
- _run_cmds(
- router,
- [
- "ip link add {0} type vrf table {1}".format(vrf, table),
- "ip link set dev {0} up".format(vrf),
- "ip link add loop{0} type dummy".format(vni),
- "ip link set dev loop{0} master {1}".format(vni, vrf),
- "ip link set dev loop{0} up".format(vni),
- "ip link add name br{0} up type bridge stp_state 0".format(vni),
- "ip link set dev br{0} master {1}".format(vni, vrf),
- "ip link set dev br{0} up".format(vni),
- (
- "ip link add name vxlan{0} type vxlan id {0} dstport 4789 "
- "dev {1}-eth0 local {2} nolearning"
- ).format(vni, rname, local_vtep),
- "ip link set dev vxlan{0} master br{0}".format(vni),
- (
- "ip link set dev vxlan{0} up type bridge_slave "
- "learning off flood off mcast_flood off"
- ).format(vni),
- ],
- )
-
-
def setup_module(mod):
- "Sets up the pytest environment"
-
- tgen = Topogen(build_topo, mod.__name__)
- tgen.start_topology()
+ "Create router VRFs and L3VNIs, load configs, and start the topology."
krel = platform.release()
if topotest.version_cmp(krel, "4.18") < 0:
pytest.skip(
- 'EVPN type-5 VRF-leak test requires kernel >= 4.18, found "{}"'.format(
- krel
- )
+ 'EVPN type-5 VRF-leak test requires kernel >= 4.18, found "{}"'.format(krel)
)
- _add_vrf_vni(tgen.gears["r1"], "r1", "GREEN", 101, 101, "192.168.100.1")
- _add_vrf_vni(tgen.gears["r2"], "r2", "GREEN", 101, 101, "192.168.100.2")
- _add_vrf_vni(tgen.gears["r2"], "r2", "BLUE", 102, 102, "192.168.100.2")
- _add_vrf_vni(tgen.gears["r2"], "r2", "PURPLE", 103, 103, "192.168.100.2")
- _add_vrf_vni(tgen.gears["r3"], "r3", "GREEN", 101, 101, "192.168.100.3")
- _add_vrf_vni(tgen.gears["r3"], "r3", "BLUE", 102, 102, "192.168.100.3")
- _add_vrf_vni(tgen.gears["r3"], "r3", "PURPLE", 103, 103, "192.168.100.3")
+ tgen = Topogen(build_topo, mod.__name__)
+ tgen.start_topology()
+
+ vrfs_by_router = {
+ "r1": (("GREEN", 101, 101, "192.168.100.1"),),
+ "r2": (
+ ("GREEN", 101, 101, "192.168.100.2"),
+ ("BLUE", 102, 102, "192.168.100.2"),
+ ("PURPLE", 103, 103, "192.168.100.2"),
+ ),
+ "r3": (
+ ("GREEN", 101, 101, "192.168.100.3"),
+ ("BLUE", 102, 102, "192.168.100.3"),
+ ("PURPLE", 103, 103, "192.168.100.3"),
+ ),
+ }
+ for rname, vrfs in vrfs_by_router.items():
+ result = create_vrf_cfg(
+ tgen,
+ None,
+ {
+ rname: {
+ "vrfs": [{"name": vrf, "id": table} for vrf, table, _, _ in vrfs]
+ }
+ },
+ )
+ assert result is True, result
+ router = tgen.gears[rname]
+ for vrf, _, vni, local_vtep in vrfs:
+ _configure_l3vni_interfaces(router, rname, vrf, vni, local_vtep)
for rname in ("r1", "r2", "r3"):
router = tgen.gears[rname]
@@ -133,67 +192,151 @@ def setup_module(mod):
def teardown_module(_mod):
- "Teardown the pytest environment"
+ "Stop the topotest topology."
tgen = get_topogen()
tgen.stop_topology()
def _bgp_route_present(router, vrf, prefix):
+ "Return whether the VRF has a valid, non-removed path for the IPv4 prefix."
output = json.loads(
- router.vtysh_cmd(
- "show bgp vrf {0} ipv4 unicast {1} json".format(vrf, prefix)
- )
+ router.vtysh_cmd("show bgp vrf {0} ipv4 unicast {1} json".format(vrf, prefix))
or "{}"
)
for path in output.get("paths", []):
- if path.get("valid") is not False:
+ if path.get("valid") is not False and not path.get("removed", False):
return True
return False
+def _bgp_valid_path_count(router, vrf, prefix):
+ "Count valid, non-removed paths for an IPv4 prefix in a VRF."
+ output = json.loads(
+ router.vtysh_cmd("show bgp vrf {0} ipv4 unicast {1} json".format(vrf, prefix))
+ or "{}"
+ )
+ return sum(
+ path.get("valid") is not False and not path.get("removed", False)
+ for path in output.get("paths", [])
+ )
+
+
+def _wait_bgp_valid_path_count_at_least(
+ router, vrf, prefix, expected, count=60, wait=1
+):
+ "Wait until a VRF has at least the requested number of valid BGP paths."
+
+ def _check_count():
+ return _bgp_valid_path_count(router, vrf, prefix) >= expected
+
+ _, result = topotest.run_and_expect(_check_count, True, count=count, wait=wait)
+ assert result, "{0} {1} route {2} did not reach {3} valid paths\n{4}".format(
+ router.name,
+ vrf,
+ prefix,
+ expected,
+ router.vtysh_cmd("show bgp vrf {0} ipv4 unicast {1}".format(vrf, prefix)),
+ )
+
+
def _wait_bgp_route(router, vrf, prefix, expected, count=60, wait=1):
+ "Wait for an IPv4 route to become present or absent in a VRF."
+
def _check_route():
return _bgp_route_present(router, vrf, prefix)
- _, result = topotest.run_and_expect(
- _check_route, expected, count=count, wait=wait
- )
- assert result == expected, (
- "{0} {1} route {2} expected={3} last_seen={4}\n{5}".format(
- router.name,
- vrf,
- prefix,
- expected,
- result,
- router.vtysh_cmd(
- "show bgp vrf {0} ipv4 unicast {1}".format(vrf, prefix)
- ),
- )
+ _, result = topotest.run_and_expect(_check_route, expected, count=count, wait=wait)
+ assert (
+ result == expected
+ ), "{0} {1} route {2} expected={3} last_seen={4}\n{5}".format(
+ router.name,
+ vrf,
+ prefix,
+ expected,
+ result,
+ router.vtysh_cmd("show bgp vrf {0} ipv4 unicast {1}".format(vrf, prefix)),
)
def _configure_static_route(router, vrf, prefix, present):
- command = "ip route {0} Null0".format(prefix)
- if not present:
+ "Add or remove a Null0 static route through the shared route API."
+ result = create_static_routes(
+ get_topogen(),
+ {
+ router.name: {
+ "static_routes": [
+ {
+ "network": prefix,
+ "interface": "Null0",
+ "vrf": vrf,
+ "delete": not present,
+ }
+ ]
+ }
+ },
+ )
+ assert result is True, result
+
+
+def _reload_frr_config(router, config_path):
+ "Apply an FRR config file with frr-reload and report command failures."
+ command = "/usr/lib/frr/frr-reload.py --reload {0}".format(config_path)
+ result, output, error = router.net.cmd_status(command, warn=False)
+ assert (
+ result == 0
+ ), "{0}: frr-reload failed for {1} (rc={2})\nstdout:\n{3}\nstderr:\n{4}".format(
+ router.name, config_path, result, output, error
+ )
+
+
+def _configure_skip_evpn_imported(router, vrf, enabled):
+ "Enable or disable IPv4 skip-evpn-imported in a VRF EVPN address family."
+ command = "advertise ipv4 unicast skip-evpn-imported"
+ if not enabled:
command = "no {0}".format(command)
output = router.vtysh_cmd(
"configure terminal\n"
- "vrf {0}\n"
- " {1}\n"
+ "router bgp 65000 vrf {0}\n"
+ "address-family l2vpn evpn\n"
+ "{1}\n"
"end\n".format(vrf, command)
)
assert "% Unknown" not in output and "% Invalid" not in output, output
-def _configure_skip_evpn_imported(router, vrf, enabled):
- command = "advertise ipv4 unicast skip-evpn-imported"
+def _configure_vrf_import(router, destination_vrf, source_vrf, present):
+ "Add or remove an IPv4 unicast VRF import with the shared BGP API."
+ import_vrf = {"vrf": source_vrf}
+ if not present:
+ import_vrf["delete"] = True
+
+ result = create_router_bgp(
+ get_topogen(),
+ topo={"routers": {router.name: {}}},
+ input_dict={
+ router.name: {
+ "bgp": [
+ {
+ "local_as": 65000,
+ "vrf": destination_vrf,
+ "address_family": {"ipv4": {"unicast": {"import": import_vrf}}},
+ }
+ ]
+ }
+ },
+ )
+ assert result is True, result
+
+
+def _configure_ipv6_skip_evpn_imported(router, vrf, enabled):
+ "Enable or disable IPv6 skip-evpn-imported in a VRF EVPN address family."
+ command = "advertise ipv6 unicast skip-evpn-imported"
if not enabled:
command = "no {0}".format(command)
-
output = router.vtysh_cmd(
"configure terminal\n"
"router bgp 65000 vrf {0}\n"
@@ -204,74 +347,116 @@ def _configure_skip_evpn_imported(router, vrf, enabled):
assert "% Unknown" not in output and "% Invalid" not in output, output
-def _evpn_type5_prefix_count(router, prefix):
- address, prefixlen = prefix.split("/")
- evpn_prefix = "[5]:[0]:[{0}]:[{1}]".format(prefixlen, address)
- return router.vtysh_cmd("show bgp l2vpn evpn route type 5").count(evpn_prefix)
+def _vrf_bgp_config(router, vrf):
+ "Return the running BGP config section for one VRF."
+ running_config = router.vtysh_cmd("show running-config bgpd")
+ marker = "router bgp 65000 vrf {0}".format(vrf)
+ assert marker in running_config, running_config
+ config = running_config.split(marker, 1)[1]
+ return config.split("\n!", 1)[0]
def _wait_evpn_type5_prefix_count(router, prefix, expected, count=60, wait=1):
- def _check_count():
- return _evpn_type5_prefix_count(router, prefix)
+ "Wait until the EVPN table has exactly the expected Type-5 path count."
- _, result = topotest.run_and_expect(
- _check_count, expected, count=count, wait=wait
- )
- assert result == expected, (
- "{0} EVPN type-5 {1} count expected={2} last_seen={3}\n{4}".format(
- router.name,
- prefix,
- expected,
- result,
- router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
- )
+ def _check_count():
+ return evpn_type5_prefix_count(router, prefix)
+
+ _, result = topotest.run_and_expect(_check_count, expected, count=count, wait=wait)
+ assert (
+ result == expected
+ ), "{0} EVPN type-5 {1} count expected={2} last_seen={3}\n{4}".format(
+ router.name,
+ prefix,
+ expected,
+ result,
+ router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
)
def _wait_evpn_type5_prefix_count_at_least(router, prefix, expected, count=60, wait=1):
- def _check_count():
- return _evpn_type5_prefix_count(router, prefix) >= expected
+ "Wait until the EVPN table has at least the expected Type-5 path count."
- _, result = topotest.run_and_expect(
- _check_count, True, count=count, wait=wait
- )
- assert result is True, (
- "{0} EVPN type-5 {1} count expected at least {2}\n{3}".format(
- router.name,
- prefix,
- expected,
- router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
- )
+ def _check_count():
+ return evpn_type5_prefix_count(router, prefix) >= expected
+
+ _, result = topotest.run_and_expect(_check_count, True, count=count, wait=wait)
+ assert (
+ result is True
+ ), "{0} EVPN type-5 {1} count expected at least {2}\n{3}".format(
+ router.name,
+ prefix,
+ expected,
+ router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
)
-def _configure_origin_route(present):
- _configure_static_route(get_topogen().gears["r1"], "GREEN", PREFIX, present)
-
-
-def _configure_normal_leak_route(present):
- _configure_static_route(
- get_topogen().gears["r2"], "BLUE", NORMAL_LEAK_PREFIX, present
+def _evpn_route_refresh_sent(router, peers):
+ "Return the per-peer count of Route-Refresh messages sent on the session."
+ counts = {}
+ for peer in peers:
+ output = json.loads(
+ router.vtysh_cmd("show bgp neighbors {0} json".format(peer))
+ or "{}"
+ )
+ stats = output.get(peer, {}).get("messageStats", {})
+ counts[peer] = stats.get("routeRefreshSent", 0)
+ return counts
+
+
+def _wait_evpn_route_refresh_sent(router, peers, before, count=60, wait=1):
+ "Wait until a new Route-Refresh has been sent to each EVPN peer."
+
+ def _refreshed():
+ current = _evpn_route_refresh_sent(router, peers)
+ return all(current[peer] > before[peer] for peer in peers)
+
+ _, result = topotest.run_and_expect(_refreshed, True, count=count, wait=wait)
+ assert (
+ result is True
+ ), "{0}: no EVPN Route-Refresh sent to {1}; before={2} after={3}".format(
+ router.name,
+ list(peers),
+ before,
+ _evpn_route_refresh_sent(router, peers),
)
-def _withdraw_normal_leak_route():
- _configure_normal_leak_route(False)
-
-
-def _add_normal_leak_route():
- _configure_normal_leak_route(True)
-
-
-def _withdraw_origin_route():
- _configure_origin_route(False)
-
-
-def _add_origin_route():
- _configure_origin_route(True)
+def _evpn_l3vni_rd(router, vni):
+ "Return the Type-5 route distinguisher heading a VRF advertises under."
+ output = json.loads(
+ router.vtysh_cmd("show bgp l2vpn evpn vni {0} json".format(vni)) or "{}"
+ )
+ rd = output.get("rd")
+ assert rd, "{0}: no RD reported for VNI {1}\n{2}".format(router.name, vni, output)
+ return "Route Distinguisher: {0}".format(rd)
+
+
+def _wait_evpn_type5_rds(router, prefix, present=(), absent=(), count=60, wait=1):
+ "Wait until a prefix's Type-5 RDs include every present and no absent RD."
+ seen = set()
+
+ def _check_rds():
+ seen.clear()
+ seen.update(evpn_type5_route_distinguishers(router, prefix))
+ return set(present) <= seen and not set(absent) & seen
+
+ _, result = topotest.run_and_expect(_check_rds, True, count=count, wait=wait)
+ assert (
+ result is True
+ ), "{0} {1}: expected RDs {2} present and {3} absent, actual={4}\n{5}".format(
+ router.name,
+ prefix,
+ sorted(present),
+ sorted(absent),
+ sorted(seen),
+ router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
+ )
+ return set(seen)
def _dump_route_state(label, prefix, routers):
+ "Log BGP VRF routes and EVPN Type-5 routes for the requested prefix."
logger.info("=== %s: %s ===", label, prefix)
for router, vrfs in routers:
@@ -292,11 +477,8 @@ def _dump_route_state(label, prefix, routers):
)
-def _dump_prefix_state(label, routers):
- _dump_route_state(label, PREFIX, routers)
-
-
def test_normal_vrf_leak_exports_with_suppress_imported_from_evpn():
+ "Verify normal VRF-leaked routes still export while EVPN imports stay suppressed."
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
@@ -305,13 +487,13 @@ def test_normal_vrf_leak_exports_with_suppress_imported_from_evpn():
r2 = tgen.gears["r2"]
try:
- step("Inject a normal static route in r2 BLUE")
- _add_normal_leak_route()
+ logger.info("Inject a normal static route in r2 BLUE")
+ _configure_static_route(r2, "BLUE", NORMAL_LEAK_PREFIX, True)
- step("Verify r2 leaks the normal route from BLUE into GREEN")
+ logger.info("Verify r2 leaks the normal route from BLUE into GREEN")
_wait_bgp_route(r2, "GREEN", NORMAL_LEAK_PREFIX, True)
- step("Verify the leaked GREEN route is still exported as EVPN type-5")
+ logger.info("Verify the leaked GREEN route is still exported as EVPN type-5")
try:
_wait_bgp_route(r1, "GREEN", NORMAL_LEAK_PREFIX, True)
except AssertionError:
@@ -322,16 +504,16 @@ def test_normal_vrf_leak_exports_with_suppress_imported_from_evpn():
)
raise
- step("Dump normal VRF-leaked route before withdraw")
+ logger.info("Dump normal VRF-leaked route before withdraw")
_dump_route_state(
"normal VRF-leaked route before withdraw",
NORMAL_LEAK_PREFIX,
((r1, ("GREEN",)), (r2, PE_VRFS)),
)
finally:
- _withdraw_normal_leak_route()
+ _configure_static_route(r2, "BLUE", NORMAL_LEAK_PREFIX, False)
- step("Verify the normal leaked route is withdrawn cleanly")
+ logger.info("Verify the normal leaked route is withdrawn cleanly")
try:
_wait_bgp_route(r1, "GREEN", NORMAL_LEAK_PREFIX, False)
_wait_bgp_route(r2, "GREEN", NORMAL_LEAK_PREFIX, False)
@@ -343,7 +525,7 @@ def test_normal_vrf_leak_exports_with_suppress_imported_from_evpn():
)
raise
- step("Dump normal VRF-leaked route after withdraw")
+ logger.info("Dump normal VRF-leaked route after withdraw")
_dump_route_state(
"normal VRF-leaked route after withdraw",
NORMAL_LEAK_PREFIX,
@@ -363,18 +545,18 @@ def test_direct_evpn_import_is_not_reexported_without_skip():
r3 = tgen.gears["r3"]
try:
- step("Disable skip-evpn-imported only in GREEN on both PEs")
+ logger.info("Disable skip-evpn-imported only in GREEN on both PEs")
for router in (r2, r3):
_configure_skip_evpn_imported(router, "GREEN", False)
- step("Inject a new Type-5 origin in r1 GREEN")
+ logger.info("Inject a new Type-5 origin in r1 GREEN")
_configure_static_route(r1, "GREEN", DIRECT_IMPORT_PREFIX, True)
- step("Verify both PEs directly import the r1 Type-5 route into GREEN")
+ logger.info("Verify both PEs directly import the r1 Type-5 route into GREEN")
_wait_bgp_route(r2, "GREEN", DIRECT_IMPORT_PREFIX, True)
_wait_bgp_route(r3, "GREEN", DIRECT_IMPORT_PREFIX, True)
- step("Verify direct imports are not re-exported without the new option")
+ logger.info("Verify direct imports are not re-exported without the new option")
for router in (r1, r2, r3):
_wait_evpn_type5_prefix_count(router, DIRECT_IMPORT_PREFIX, 1)
finally:
@@ -383,34 +565,658 @@ def test_direct_evpn_import_is_not_reexported_without_skip():
_configure_skip_evpn_imported(router, "GREEN", True)
-def test_skip_evpn_imported_running_config():
- "Verify the option is serialized once for every configured tenant VRF."
+def test_skip_evpn_imported_survives_bgpd_restart():
+ """Verify skip state and export suppression survive a bgpd restart."""
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
- for router in (tgen.gears["r2"], tgen.gears["r3"]):
- running_config = router.vtysh_cmd("show running-config bgpd")
- logger.info(
- "%s running-config with skip-evpn-imported:\n%s",
- router.name,
- running_config,
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+
+ try:
+ logger.info("Inject a route that is subject to skip-evpn-imported")
+ _configure_static_route(r1, "GREEN", RESTART_PREFIX, True)
+
+ logger.info("Verify the route is leaked into all tenant VRFs")
+ _wait_bgp_route(r2, "GREEN", RESTART_PREFIX, True)
+ _wait_bgp_route(r2, "BLUE", RESTART_PREFIX, True)
+ _wait_bgp_route(r2, "PURPLE", RESTART_PREFIX, True)
+ _wait_bgp_route(r3, "GREEN", RESTART_PREFIX, True)
+
+ logger.info("Verify skip suppresses EVPN-derived re-export before restart")
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, RESTART_PREFIX, 1)
+
+ logger.info("Restart bgpd on one PE")
+ kill_router_daemons(tgen, "r2", ["bgpd"])
+ start_result = start_router_daemons(tgen, "r2", ["bgpd"])
+ assert not start_result, "r2 bgpd failed to start after restart:\n{0}".format(
+ start_result
)
- actual = running_config.count("advertise ipv4 unicast skip-evpn-imported")
- assert actual == len(PE_VRFS), (
- "{0} expected {1} skip-evpn-imported running-config lines, "
- "found {2}\n{3}".format(
- router.name,
+
+ _reload_frr_config(r2, "/etc/frr/frr.conf")
+ result = verify_bgp_convergence_from_running_config(tgen, dut="r2")
+ assert result is True, "r2 BGP did not reconverge: {0}".format(result)
+
+ logger.info("Verify leaked routes recover after bgpd restart")
+ _wait_bgp_route(r2, "GREEN", RESTART_PREFIX, True)
+ _wait_bgp_route(r2, "BLUE", RESTART_PREFIX, True)
+ _wait_bgp_route(r2, "PURPLE", RESTART_PREFIX, True)
+
+ logger.info("Verify skip remains enabled and re-export remains suppressed")
+ running_config = r2.vtysh_cmd("show running-config bgpd")
+ assert running_config.count("advertise ipv4 unicast skip-evpn-imported") == len(
+ PE_VRFS
+ ), (
+ "r2: expected {0} IPv4 skip-evpn-imported configuration lines, "
+ "actual={1}\n{2}".format(
len(PE_VRFS),
- actual,
+ running_config.count("advertise ipv4 unicast skip-evpn-imported"),
running_config,
)
)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, RESTART_PREFIX, 1)
+ finally:
+ _configure_static_route(r1, "GREEN", RESTART_PREFIX, False)
+
+
+def test_skip_evpn_imported_survives_bgp_session_clear():
+ """Verify suppression and route state recover after a BGP session clear."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+
+ try:
+ logger.info("Originate an EVPN-derived route with suppression enabled")
+ _configure_static_route(r1, "GREEN", SESSION_CLEAR_PREFIX, True)
+ for router in (r2, r3):
+ for vrf in ("GREEN", "BLUE", "PURPLE"):
+ _wait_bgp_route(router, vrf, SESSION_CLEAR_PREFIX, True)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, SESSION_CLEAR_PREFIX, 1)
+
+ logger.info("Clear all BGP sessions on one PE")
+ output = r2.vtysh_cmd("clear bgp *")
+ assert "% Unknown" not in output and "% Invalid" not in output, output
+
+ logger.info("Reload r2's existing unified configuration")
+ _reload_frr_config(r2, "/etc/frr/frr.conf")
+ result = verify_bgp_convergence_from_running_config(tgen, dut="r2")
+ assert result is True, "r2 BGP did not reconverge: {0}".format(result)
+
+ logger.info("Verify BGP routes reconverge after the session clear")
+ for router in (r2, r3):
+ for vrf in ("GREEN", "BLUE", "PURPLE"):
+ _wait_bgp_route(router, vrf, SESSION_CLEAR_PREFIX, True)
+
+ logger.info("Verify skip remains configured and no re-export is created")
+ for vrf in ("GREEN", "BLUE", "PURPLE"):
+ vrf_config = _vrf_bgp_config(r2, vrf)
+ assert (
+ "advertise ipv4 unicast skip-evpn-imported" in vrf_config
+ ), "r2 {0}: expected IPv4 skip-evpn-imported enabled\n{1}".format(
+ vrf, vrf_config
+ )
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, SESSION_CLEAR_PREFIX, 1)
+ finally:
+ _configure_static_route(r1, "GREEN", SESSION_CLEAR_PREFIX, False)
+ for router in (r2, r3):
+ for vrf in ("GREEN", "BLUE", "PURPLE"):
+ _configure_skip_evpn_imported(router, vrf, True)
+
+
+def test_skip_evpn_imported_ipv6_is_independent_from_ipv4():
+ """Verify IPv6 skip state does not change IPv4 skip state."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ pe_routers = (tgen.gears["r2"], tgen.gears["r3"])
+ try:
+ logger.info("Enable IPv6 skip-evpn-imported on BLUE")
+ for router in pe_routers:
+ _configure_ipv6_skip_evpn_imported(router, "BLUE", True)
+ blue_config = _vrf_bgp_config(router, "BLUE")
+ assert (
+ "advertise ipv4 unicast skip-evpn-imported" in blue_config
+ ), "{0} BLUE: expected IPV4 skip-evpn-imported enabled\n{1}".format(
+ router.name, blue_config
+ )
+ assert (
+ "advertise ipv6 unicast skip-evpn-imported" in blue_config
+ ), "{0} BLUE: expected IPV6 skip-evpn-imported enabled\n{1}".format(
+ router.name, blue_config
+ )
+
+ logger.info("Disable only IPv6 skip-evpn-imported")
+ for router in pe_routers:
+ _configure_ipv6_skip_evpn_imported(router, "BLUE", False)
+ blue_config = _vrf_bgp_config(router, "BLUE")
+ assert (
+ "advertise ipv4 unicast skip-evpn-imported" in blue_config
+ ), "{0} BLUE: expected IPV4 skip-evpn-imported enabled\n{1}".format(
+ router.name, blue_config
+ )
+ assert (
+ "advertise ipv6 unicast skip-evpn-imported" not in blue_config
+ ), "{0} BLUE: expected IPV6 skip-evpn-imported disabled\n{1}".format(
+ router.name, blue_config
+ )
+
+ finally:
+ for router in pe_routers:
+ _configure_ipv6_skip_evpn_imported(router, "BLUE", False)
+
+
+def test_skip_evpn_imported_preserves_local_type5_export():
+ """Verify skip does not change existing L3VNI control or route behavior."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ try:
+ logger.info("Verify skip is rendered under the BLUE VRF EVPN AF")
+ blue_config = _vrf_bgp_config(r2, "BLUE")
+ assert (
+ "advertise ipv4 unicast skip-evpn-imported" in blue_config
+ ), "{0} BLUE: expected IPV4 skip-evpn-imported enabled\n{1}".format(
+ r2.name, blue_config
+ )
+
+ logger.info("Verify BGP control-plane state is available before the route test")
+ for router in (r1, r2, r3):
+ summary = router.vtysh_cmd("show bgp summary")
+ assert "% Unknown" not in summary and "% Invalid" not in summary, summary
+
+ evpn_summary = json.loads(
+ router.vtysh_cmd("show bgp l2vpn evpn summary json") or "{}"
+ )
+ # EVPN summary JSON exposes established peers through the
+ # displayed-peer count; individual peer objects do not carry the
+ # text-summary state field in all supported FRR versions.
+ assert (
+ evpn_summary.get("displayedPeers", 0) > 0
+ ), "{0}: expected EVPN displayedPeers > 0, actual={1}\n{2}".format(
+ router.name, evpn_summary.get("displayedPeers", 0), evpn_summary
+ )
+
+ for router in (r2, r3):
+ vni_output = router.vtysh_cmd("show evpn vni 102")
+ assert (
+ "Type: L3" in vni_output and "BLUE" in vni_output
+ ), "{0}: expected VNI 102 type L3 in BLUE\n{1}".format(
+ router.name, vni_output
+ )
+
+ logger.info("Add a locally originated route in the BLUE L3VNI")
+ _configure_static_route(r2, "BLUE", L3VNI_PREFIX, True)
+ _wait_bgp_route(r2, "BLUE", L3VNI_PREFIX, True)
+
+ logger.info("Verify the representative BLUE route is installed in Zebra RIB")
+ expected_zebra_route = {
+ L3VNI_PREFIX: [
+ {
+ "protocol": "static",
+ "vrfName": "BLUE",
+ "selected": True,
+ "installed": True,
+ }
+ ]
+ }
+ _, result = topotest.run_and_expect(
+ lambda: evpn_verify_vrf_rib_route(
+ r2, "BLUE", L3VNI_PREFIX, expected_zebra_route
+ ),
+ None,
+ count=30,
+ wait=1,
+ )
+ assert result is None, "r2 BLUE {0}: expected route in Zebra RIB: {1}".format(
+ L3VNI_PREFIX, result
+ )
+
+ logger.info("Verify the representative BLUE route is installed in kernel FIB")
+ expected_kernel_route = [{"dst": L3VNI_PREFIX}]
+ _, result = topotest.run_and_expect(
+ lambda: iproute2_check_path_selection(
+ r2,
+ L3VNI_PREFIX,
+ expected_kernel_route,
+ vrf_name="BLUE",
+ ),
+ None,
+ count=30,
+ wait=1,
+ )
+ assert result is None, "r2 BLUE {0}: expected route in kernel FIB: {1}".format(
+ L3VNI_PREFIX, result
+ )
+
+ logger.info("Verify a local L3VNI route remains exported with skip enabled")
+ for router in (r1, r2, r3):
+ # The local route can be exported through more than one valid
+ # RD after local VRF leaking. Only require that export remains.
+ _wait_evpn_type5_prefix_count_at_least(router, L3VNI_PREFIX, 1)
+ finally:
+ _configure_static_route(r2, "BLUE", L3VNI_PREFIX, False)
+
+
+def test_skip_evpn_imported_scale_and_repeated_toggle():
+ """Verify 1,000 Type-5 routes across two skip enable/disable cycles."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ prefixes = [
+ "198.18.{0}.{1}/32".format(index // 256, index % 256)
+ for index in range(1, 1001)
+ ]
+ prefix_addresses = [prefix.split("/", 1)[0] for prefix in prefixes]
+ scale_deadline = time.monotonic() + 30 * 60
+
+ def wait_for_counts(router, minimum, phase, exact=False):
+ remaining = scale_deadline - time.monotonic()
+ context = "{0}: {1}, expected {2} Type-5 copies per prefix ({3})".format(
+ router.name, phase, minimum, "exact" if exact else "at least"
+ )
+ assert remaining > 0, "{0}: scale convergence budget expired".format(context)
+ mismatches = {}
+
+ def _check_counts():
+ nonlocal mismatches
+ output = router.vtysh_cmd("show bgp l2vpn evpn route type 5")
+ counts = {}
+ for address in re.findall(r"\[5\]:\[0\]:\[32\]:\[([^]]+)\]", output):
+ counts[address] = counts.get(address, 0) + 1
+ mismatches = {
+ address: counts.get(address, 0)
+ for address in prefix_addresses
+ if (
+ counts.get(address, 0) != minimum
+ if exact
+ else counts.get(address, 0) < minimum
+ )
+ }
+ return not mismatches
+
+ retry_count = max(1, min(180, int(remaining // 2)))
+ _, result = topotest.run_and_expect(
+ _check_counts, True, count=retry_count, wait=2
+ )
+ assert (
+ result
+ ), "{0}: {1}/{2} prefixes mismatched; first 20 actual counts: {3}".format(
+ context,
+ len(mismatches),
+ len(prefix_addresses),
+ list(mismatches.items())[:20],
+ )
+
+ try:
+ logger.info("Inject 1,000 Type-5 origins in r1 GREEN")
+ result = create_static_routes(
+ tgen,
+ {
+ r1.name: {
+ "static_routes": [
+ {
+ "network": prefixes,
+ "interface": "Null0",
+ "vrf": "GREEN",
+ }
+ ]
+ }
+ },
+ )
+ assert result is True, result
+
+ logger.info("Verify all 1,000 prefixes are initially exported once")
+ for router in (r1, r2, r3):
+ wait_for_counts(router, 1, "initial advertisement", exact=True)
+
+ for cycle in range(2):
+ logger.info("Disable skip for scale cycle {0}".format(cycle + 1))
+ for router in (r2, r3):
+ _configure_skip_evpn_imported(router, "BLUE", False)
+ wait_for_counts(r1, 2, "cycle {0}, skip disabled".format(cycle + 1))
+
+ logger.info("Re-enable skip for scale cycle {0}".format(cycle + 1))
+ for router in (r2, r3):
+ _configure_skip_evpn_imported(router, "BLUE", True)
+ wait_for_counts(
+ r1, 1, "cycle {0}, skip enabled".format(cycle + 1), exact=True
+ )
+ finally:
+ result = create_static_routes(
+ tgen,
+ {
+ r1.name: {
+ "static_routes": [
+ {
+ "network": prefixes,
+ "interface": "Null0",
+ "vrf": "GREEN",
+ "delete": True,
+ }
+ ]
+ }
+ },
+ )
+ assert result is True, result
+ for router in (r2, r3):
+ _configure_skip_evpn_imported(router, "BLUE", True)
+
+
+def test_skip_evpn_imported_survives_route_refresh():
+ """Verify a BGP route refresh does not re-export a suppressed path."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ prefix = "192.168.94.1/32"
+
+ try:
+ logger.info("Establish an EVPN-originated leaked route with skip enabled")
+ _configure_static_route(r1, "GREEN", prefix, True)
+ for router, vrf in (
+ (r2, "GREEN"),
+ (r2, "BLUE"),
+ (r3, "GREEN"),
+ (r3, "BLUE"),
+ ):
+ _wait_bgp_route(router, vrf, prefix, True)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, prefix, 1)
+
+ logger.info("Reload r2's existing unified configuration before refresh")
+ _reload_frr_config(r2, "/etc/frr/frr.conf")
+ result = verify_bgp_convergence_from_running_config(tgen, dut="r2")
+ assert result is True, "r2 BGP did not reconverge: {0}".format(result)
+
+ # The tenant VRF instances have no neighbors of their own: the leaked
+ # route arrives over the fabric sessions in the default EVPN instance,
+ # so the refresh has to target those peers to reach the Type-5 path.
+ logger.info("Trigger a soft inbound EVPN refresh on the fabric peers")
+ for router in (r2, r3):
+ peers = FABRIC_PEERS[router.name]
+ before = _evpn_route_refresh_sent(router, peers)
+ for peer in peers:
+ output = router.vtysh_cmd(
+ "clear bgp l2vpn evpn {0} soft in".format(peer)
+ )
+ assert "% Unknown" not in output and "% Invalid" not in output, output
+ _wait_evpn_route_refresh_sent(router, peers, before)
+
+ logger.info("Verify refreshed routes remain installed in tenant VRFs")
+ for router, vrf in (
+ (r2, "GREEN"),
+ (r2, "BLUE"),
+ (r2, "PURPLE"),
+ (r3, "GREEN"),
+ (r3, "BLUE"),
+ (r3, "PURPLE"),
+ ):
+ _wait_bgp_route(router, vrf, prefix, True)
+
+ logger.info("Verify refresh did not create a local Type-5 re-export")
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, prefix, 1)
+ finally:
+ _configure_static_route(r1, "GREEN", prefix, False)
+ for router in (r2, r3):
+ _configure_skip_evpn_imported(router, "BLUE", True)
+
+
+def test_skip_evpn_imported_preserves_default_route_export():
+ """Verify a locally originated default remains eligible for Type-5 export."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r2 = tgen.gears["r2"]
+ default_prefix = "0.0.0.0/0"
+
+ try:
+ logger.info("Install a local default route in r2 BLUE")
+ _configure_static_route(r2, "BLUE", default_prefix, True)
+ _wait_bgp_route(r2, "BLUE", default_prefix, True)
+
+ logger.info("Verify skip does not suppress the locally originated default")
+ expected = "[5]:[0]:[0]:[0.0.0.0]"
+ for router in (tgen.gears["r1"], r2, tgen.gears["r3"]):
+
+ def _default_exported(router=router):
+ return expected in router.vtysh_cmd("show bgp l2vpn evpn route type 5")
+
+ _, result = topotest.run_and_expect(
+ _default_exported, True, count=60, wait=1
+ )
+ assert (
+ result
+ ), "{0} {1}: expected default Type-5 export present, actual absent\n{2}".format(
+ router.name,
+ default_prefix,
+ router.vtysh_cmd("show bgp l2vpn evpn route type 5"),
+ )
+ finally:
+ _configure_static_route(r2, "BLUE", default_prefix, False)
+
+
+def test_skip_evpn_imported_prefix_move_between_vrfs():
+ """Verify a moved prefix converges without retaining the old Type-5."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ prefix = "192.168.96.1/32"
+
+ try:
+ logger.info("Originate P in the initial PE r1 GREEN")
+ _configure_static_route(r1, "GREEN", prefix, True)
+ _wait_bgp_route(r2, "BLUE", prefix, True)
+ _wait_bgp_route(r2, "PURPLE", prefix, True)
+ old_rds = evpn_type5_route_distinguishers(r1, prefix)
+ assert old_rds, r1.vtysh_cmd("show bgp l2vpn evpn route type 5")
+
+ logger.info("Withdraw P from the old origin and verify old state is removed")
+ _configure_static_route(r1, "GREEN", prefix, False)
+ for router in (r1, r2, tgen.gears["r3"]):
+ _wait_evpn_type5_prefix_count(router, prefix, 0)
+ for router, vrf in (
+ (r1, "GREEN"),
+ (r2, "GREEN"),
+ (r2, "BLUE"),
+ (r2, "PURPLE"),
+ (tgen.gears["r3"], "GREEN"),
+ (tgen.gears["r3"], "BLUE"),
+ (tgen.gears["r3"], "PURPLE"),
+ ):
+ _wait_bgp_route(router, vrf, prefix, False)
+
+ logger.info("Originate P from the new PE r2 BLUE")
+ _configure_static_route(r2, "BLUE", prefix, True)
+ _wait_bgp_route(r2, "BLUE", prefix, True)
+ _wait_evpn_type5_prefix_count_at_least(r2, prefix, 1)
+ new_rds = evpn_type5_route_distinguishers(r2, prefix)
+ assert new_rds, r2.vtysh_cmd("show bgp l2vpn evpn route type 5")
+
+ logger.info("Verify VRF leaking converges to the new origin")
+ for router, vrf in (
+ (r2, "GREEN"),
+ (r2, "PURPLE"),
+ (tgen.gears["r3"], "GREEN"),
+ (tgen.gears["r3"], "BLUE"),
+ (tgen.gears["r3"], "PURPLE"),
+ (r1, "GREEN"),
+ ):
+ _wait_bgp_route(router, vrf, prefix, True)
+
+ logger.info("Verify the new Type-5 is present without the old origin")
+ for router in (r1, r2, tgen.gears["r3"]):
+ _wait_evpn_type5_prefix_count_at_least(router, prefix, 1)
+ current_rds = evpn_type5_route_distinguishers(router, prefix)
+ assert not set(old_rds).intersection(
+ current_rds
+ ), "{0} retained old Type-5 origin {1}: {2}".format(
+ router.name, old_rds, current_rds
+ )
+ finally:
+ _configure_static_route(r1, "GREEN", prefix, False)
+ _configure_static_route(r2, "BLUE", prefix, False)
+
+
+def test_skip_evpn_imported_coexists_with_route_map():
+ """Verify route-map filtering and skip options can be changed independently."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ route_map = "evpn_skip_test_map"
+ prefix_list = "evpn_skip_test_prefixes"
+ allowed_prefix = "192.168.97.1/32"
+ denied_prefix = "192.168.97.2/32"
+
+ def configure_route_map(enabled):
+ commands = ["configure terminal"]
+ if enabled:
+ commands.extend(
+ [
+ "ip prefix-list {0} seq 10 permit {1}".format(
+ prefix_list, allowed_prefix
+ ),
+ "route-map {0} permit 10".format(route_map),
+ "match ip address prefix-list {0}".format(prefix_list),
+ "exit",
+ ]
+ )
+
+ for vrf in PE_VRFS:
+ if enabled:
+ advertise = (
+ "advertise ipv4 unicast route-map {0} skip-evpn-imported".format(
+ route_map
+ )
+ )
+ else:
+ advertise = "no advertise ipv4 unicast route-map {0}".format(route_map)
+ commands.extend(
+ [
+ "router bgp 65000 vrf {0}".format(vrf),
+ "address-family l2vpn evpn",
+ advertise,
+ "exit-address-family",
+ "exit",
+ ]
+ )
+
+ if not enabled:
+ commands.extend(
+ [
+ "no route-map {0}".format(route_map),
+ "no ip prefix-list {0}".format(prefix_list),
+ ]
+ )
+ commands.append("end")
+ output = r2.vtysh_cmd("\n".join(commands))
+ assert "% Unknown" not in output and "% Invalid" not in output, output
+
+ try:
+ logger.info("Inject an EVPN-derived route and local route-map test routes")
+ _configure_static_route(r1, "GREEN", TOGGLE_PREFIX, True)
+ _configure_static_route(r2, "BLUE", allowed_prefix, True)
+ _configure_static_route(r2, "BLUE", denied_prefix, True)
+ _wait_bgp_route(r2, "BLUE", allowed_prefix, True)
+ _wait_bgp_route(r2, "BLUE", denied_prefix, True)
+
+ logger.info("Attach a route-map to all r2 tenant VRFs")
+ configure_route_map(True)
+ blue_config = _vrf_bgp_config(r2, "BLUE")
+ assert (
+ "advertise ipv4 unicast route-map {0} skip-evpn-imported".format(route_map)
+ in blue_config
+ ), "r2 BLUE: expected route-map {0} and skip-evpn-imported\n{1}".format(
+ route_map, blue_config
+ )
+
+ logger.info("Verify route-map filtering and EVPN ancestry suppression")
+ _wait_evpn_type5_prefix_count_at_least(r1, allowed_prefix, 1)
+ _wait_evpn_type5_prefix_count(r1, denied_prefix, 0)
+ _wait_evpn_type5_prefix_count(r1, TOGGLE_PREFIX, 1)
+
+ logger.info("Remove the route-map while keeping skip enabled")
+ configure_route_map(False)
+ blue_config = _vrf_bgp_config(r2, "BLUE")
+ assert (
+ "advertise ipv4 unicast skip-evpn-imported" in blue_config
+ ), "{0} BLUE: expected IPV4 skip-evpn-imported enabled\n{1}".format(
+ r2.name, blue_config
+ )
+ assert (
+ "route-map {0}".format(route_map) not in blue_config
+ ), "r2 BLUE: expected route-map {0} removed\n{1}".format(route_map, blue_config)
+ _wait_evpn_type5_prefix_count_at_least(r1, denied_prefix, 1)
+
+ logger.info("Reattach the route-map and remove only skip from BLUE")
+ configure_route_map(True)
+ _configure_skip_evpn_imported(r2, "BLUE", False)
+ blue_config = _vrf_bgp_config(r2, "BLUE")
+ assert (
+ "advertise ipv4 unicast route-map {0}".format(route_map) in blue_config
+ ), "r2 BLUE: expected IPv4 advertisement with route-map {0}\n{1}".format(
+ route_map, blue_config
+ )
+ assert (
+ "skip-evpn-imported" not in blue_config
+ ), "{0} BLUE: expected skip-evpn-imported disabled\n{1}".format(
+ r2.name, blue_config
+ )
+ _wait_evpn_type5_prefix_count(r1, denied_prefix, 0)
+ _wait_evpn_type5_prefix_count(r1, TOGGLE_PREFIX, 1)
+
+ logger.info("Remove the route-map and verify EVPN re-export resumes")
+ configure_route_map(False)
+ _wait_evpn_type5_prefix_count_at_least(r1, TOGGLE_PREFIX, 2)
+ finally:
+ _configure_static_route(r1, "GREEN", TOGGLE_PREFIX, False)
+ _configure_static_route(r2, "BLUE", allowed_prefix, False)
+ _configure_static_route(r2, "BLUE", denied_prefix, False)
+ configure_route_map(False)
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(r2, vrf, True)
def test_no_skip_evpn_imported_reexports_and_reenabling_cleans_up():
- "Verify no/enable transitions refresh Type-5 export eligibility."
+ "Verify toggle transitions update configuration and Type-5 export eligibility."
tgen = get_topogen()
if tgen.routers_have_failure():
@@ -421,28 +1227,51 @@ def test_no_skip_evpn_imported_reexports_and_reenabling_cleans_up():
r3 = tgen.gears["r3"]
pe_routers = (r2, r3)
+ def check_running_config(enabled):
+ marker = "advertise ipv4 unicast skip-evpn-imported"
+ expected = len(PE_VRFS) if enabled else 0
+ for router in pe_routers:
+ running_config = router.vtysh_cmd("show running-config bgpd")
+ actual = running_config.count(marker)
+ assert actual == expected, (
+ "{0} expected {1} skip-evpn-imported running-config lines, "
+ "found {2}\n{3}".format(router.name, expected, actual, running_config)
+ )
+ for vrf in PE_VRFS:
+ vrf_config = _vrf_bgp_config(router, vrf)
+ assert vrf_config.count(marker) == int(
+ enabled
+ ), "{0} {1}: unexpected skip-evpn-imported configuration\n{2}".format(
+ router.name, vrf, vrf_config
+ )
+
try:
- step("Inject a Type-5 origin while skip-evpn-imported is enabled")
+ check_running_config(True)
+ logger.info("Inject a Type-5 origin while skip-evpn-imported is enabled")
_configure_static_route(r1, "GREEN", TOGGLE_PREFIX, True)
- step("Verify the original Type-5 route is the only EVPN route")
+ logger.info("Verify the original Type-5 route is the only EVPN route")
for router in (r1, r2, r3):
_wait_evpn_type5_prefix_count(router, TOGGLE_PREFIX, 1)
- step("Remove skip-evpn-imported from all PE tenant VRFs")
+ logger.info("Remove skip-evpn-imported from all PE tenant VRFs")
for router in pe_routers:
for vrf in PE_VRFS:
_configure_skip_evpn_imported(router, vrf, False)
- step("Verify removing the option restores EVPN-origin re-export")
+ check_running_config(False)
+
+ logger.info("Verify removing the option restores EVPN-origin re-export")
_wait_evpn_type5_prefix_count_at_least(r1, TOGGLE_PREFIX, 2)
- step("Re-enable skip-evpn-imported on all PE tenant VRFs")
+ logger.info("Re-enable skip-evpn-imported on all PE tenant VRFs")
for router in pe_routers:
for vrf in PE_VRFS:
_configure_skip_evpn_imported(router, vrf, True)
- step("Verify re-enabling the option withdraws re-exported copies")
+ check_running_config(True)
+
+ logger.info("Verify re-enabling the option withdraws re-exported copies")
for router in (r1, r2, r3):
_wait_evpn_type5_prefix_count(router, TOGGLE_PREFIX, 1)
finally:
@@ -452,7 +1281,9 @@ def test_no_skip_evpn_imported_reexports_and_reenabling_cleans_up():
_configure_skip_evpn_imported(router, vrf, True)
-def test_evpn_type5_vrf_leak_stale_after_origin_withdraw():
+def test_skip_evpn_imported_withdrawal_is_vrf_scoped():
+ """Withdraw one L3VNI copy without removing the other VRF copies."""
+
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
@@ -460,56 +1291,391 @@ def test_evpn_type5_vrf_leak_stale_after_origin_withdraw():
r1 = tgen.gears["r1"]
r2 = tgen.gears["r2"]
r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
try:
- step("Inject the static origin route in r1 GREEN")
- _add_origin_route()
+ logger.info("Allow Type-5 exports for the shared-prefix baseline")
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, False)
- try:
- step("Verify both PEs import r1 EVPN-origin route into GREEN")
- _wait_bgp_route(r2, "GREEN", PREFIX, True)
- _wait_bgp_route(r3, "GREEN", PREFIX, True)
+ # Identify the exact RDs each PE advertises the BLUE and PURPLE copies
+ # under, so the remote checks below do not depend on a racy RD count.
+ blue_rds = {_evpn_l3vni_rd(router, BLUE_L3VNI) for router in pe_routers}
+ purple_rds = {_evpn_l3vni_rd(router, PURPLE_L3VNI) for router in pe_routers}
- step("Verify both PEs leak the EVPN-origin route into BLUE and PURPLE")
- for router in (r2, r3):
- _wait_bgp_route(router, "BLUE", PREFIX, True)
- _wait_bgp_route(router, "PURPLE", PREFIX, True)
- except AssertionError:
- _dump_prefix_state(
- "EVPN-origin route before withdraw failure",
- ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
- )
- raise
+ logger.info("Originate one prefix and leak it into multiple L3VNIs")
+ _configure_static_route(r1, "GREEN", SHARED_RD_PREFIX, True)
+ for router in pe_routers:
+ _wait_bgp_route(router, "GREEN", SHARED_RD_PREFIX, True)
+ _wait_bgp_route(router, "BLUE", SHARED_RD_PREFIX, True)
+ _wait_bgp_route(router, "PURPLE", SHARED_RD_PREFIX, True)
+
+ # r1 receiving the exports is a convergence step of its own: the PEs
+ # having installed the route locally does not mean r1 has them yet.
+ before_rds = _wait_evpn_type5_rds(
+ r1, SHARED_RD_PREFIX, present=blue_rds | purple_rds
+ )
+
+ logger.info("Remove all BLUE import paths for the shared prefix")
+ for router in pe_routers:
+ _configure_vrf_import(router, "BLUE", "GREEN", False)
+ _configure_vrf_import(router, "BLUE", "PURPLE", False)
- step("Dump EVPN-origin route before withdraw")
- _dump_prefix_state(
- "EVPN-origin route before withdraw",
- ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
+ logger.info("Verify withdrawal is scoped to BLUE's L3VNI")
+ for router in pe_routers:
+ _wait_bgp_route(router, "BLUE", SHARED_RD_PREFIX, False)
+ _wait_bgp_route(router, "PURPLE", SHARED_RD_PREFIX, True)
+ after_rds = _wait_evpn_type5_rds(
+ r1, SHARED_RD_PREFIX, present=purple_rds, absent=blue_rds
)
+ logger.info(
+ "r1 %s Type-5 RDs: before=%s after=%s",
+ SHARED_RD_PREFIX,
+ sorted(before_rds),
+ sorted(after_rds),
+ )
+ finally:
+ for router in pe_routers:
+ _configure_vrf_import(router, "BLUE", "GREEN", True)
+ _configure_vrf_import(router, "BLUE", "PURPLE", True)
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+ _configure_static_route(r1, "GREEN", SHARED_RD_PREFIX, False)
- step("Withdraw the origin prefix from r1 GREEN")
- _withdraw_origin_route()
- step("Verify no stale leaked/re-exported copy remains on either PE")
- try:
- _wait_bgp_route(r1, "GREEN", PREFIX, False)
- for router in (r2, r3):
+def test_skip_evpn_imported_multiple_source_vrfs_to_purple():
+ """Suppress EVPN-derived routes from multiple source VRFs in PURPLE."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
+
+ try:
+ logger.info("Originate EVPN routes from separate GREEN and BLUE source VRFs")
+ _configure_static_route(r1, "GREEN", MULTI_SOURCE_GREEN_PREFIX, True)
+ _configure_static_route(r2, "BLUE", MULTI_SOURCE_BLUE_PREFIX, True)
+ for router in pe_routers:
+ _wait_bgp_route(router, "PURPLE", MULTI_SOURCE_GREEN_PREFIX, True)
+ _wait_bgp_route(router, "PURPLE", MULTI_SOURCE_BLUE_PREFIX, True)
+
+ logger.info("Verify local and local-leak Type-5 paths remain eligible")
+ # Both origins are local to the fabric. skip-evpn-imported must not
+ # suppress the original paths or their legitimate local-VRF leaks.
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count_at_least(router, MULTI_SOURCE_GREEN_PREFIX, 1)
+ _wait_evpn_type5_prefix_count_at_least(router, MULTI_SOURCE_BLUE_PREFIX, 3)
+ finally:
+ _configure_static_route(r1, "GREEN", MULTI_SOURCE_GREEN_PREFIX, False)
+ _configure_static_route(r2, "BLUE", MULTI_SOURCE_BLUE_PREFIX, False)
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+
+def test_skip_evpn_imported_suppresses_after_initial_vrf_import():
+ """Suppress a route when a VRF import is added after route arrival."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
+
+ try:
+ logger.info("Remove PURPLE imports from GREEN and BLUE before route arrival")
+ for router in pe_routers:
+ _configure_vrf_import(router, "PURPLE", "GREEN", False)
+ _configure_vrf_import(router, "PURPLE", "BLUE", False)
+
+ logger.info("Originate the route while PURPLE cannot import GREEN")
+ _configure_static_route(r1, "GREEN", INITIAL_IMPORT_PREFIX, True)
+ for router in pe_routers:
+ _wait_bgp_route(router, "GREEN", INITIAL_IMPORT_PREFIX, True)
+ _wait_bgp_route(router, "PURPLE", INITIAL_IMPORT_PREFIX, False)
+
+ logger.info("Add the PURPLE import and verify first import is suppressed")
+ for router in pe_routers:
+ _configure_vrf_import(router, "PURPLE", "GREEN", True)
+ _wait_bgp_route(router, "PURPLE", INITIAL_IMPORT_PREFIX, True)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, INITIAL_IMPORT_PREFIX, 1)
+ finally:
+ _configure_static_route(r1, "GREEN", INITIAL_IMPORT_PREFIX, False)
+ for router in pe_routers:
+ _configure_vrf_import(router, "PURPLE", "GREEN", True)
+ _configure_vrf_import(router, "PURPLE", "BLUE", True)
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+
+def test_skip_evpn_imported_selectively_withdraws_evpn_derived_paths():
+ """Withdraw EVPN-derived Type-5 paths without removing local exports."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
+
+ try:
+ logger.info("Temporarily allow all Type-5 exports for the baseline")
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, False)
+
+ logger.info("Create EVPN-derived and locally originated control routes")
+ _configure_static_route(r1, "GREEN", SELECTIVE_EVPN_PREFIX, True)
+ _configure_static_route(r2, "BLUE", SELECTIVE_LOCAL_PREFIX, True)
+ _wait_bgp_route(r2, "BLUE", SELECTIVE_EVPN_PREFIX, True)
+ _wait_bgp_route(r3, "BLUE", SELECTIVE_EVPN_PREFIX, True)
+ _wait_evpn_type5_prefix_count_at_least(r1, SELECTIVE_EVPN_PREFIX, 2)
+ _wait_evpn_type5_prefix_count_at_least(r1, SELECTIVE_LOCAL_PREFIX, 1)
+
+ logger.info("Enable suppression on the tenant VRFs")
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+ logger.info("Verify only the EVPN-derived Type-5 copies are withdrawn")
+ _wait_evpn_type5_prefix_count(r1, SELECTIVE_EVPN_PREFIX, 1)
+ _wait_evpn_type5_prefix_count_at_least(r1, SELECTIVE_LOCAL_PREFIX, 1)
+ _wait_bgp_route(r2, "BLUE", SELECTIVE_LOCAL_PREFIX, True)
+ finally:
+ _configure_static_route(r1, "GREEN", SELECTIVE_EVPN_PREFIX, False)
+ _configure_static_route(r2, "BLUE", SELECTIVE_LOCAL_PREFIX, False)
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+
+def test_skip_evpn_imported_cleans_up_after_vrf_import_removal():
+ """Remove a VRF import and verify its stale Type-5 copy is withdrawn."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
+
+ try:
+ logger.info("Allow BLUE Type-5 re-export for the cleanup baseline")
+ for router in pe_routers:
+ _configure_skip_evpn_imported(router, "BLUE", False)
+
+ logger.info("Originate a route and leak it from GREEN into BLUE")
+ _configure_static_route(r1, "GREEN", VRF_IMPORT_PREFIX, True)
+ for router in pe_routers:
+ _wait_bgp_route(router, "GREEN", VRF_IMPORT_PREFIX, True)
+ _wait_bgp_route(router, "BLUE", VRF_IMPORT_PREFIX, True)
+ _wait_evpn_type5_prefix_count_at_least(r1, VRF_IMPORT_PREFIX, 2)
+
+ logger.info("Remove the GREEN import from BLUE on both PEs")
+ for router in pe_routers:
+ _configure_vrf_import(router, "BLUE", "GREEN", False)
+
+ logger.info("Verify the leaked route and stale Type-5 copy are removed")
+ for router in pe_routers:
+ _wait_bgp_route(router, "BLUE", VRF_IMPORT_PREFIX, False)
+ _wait_evpn_type5_prefix_count(r1, VRF_IMPORT_PREFIX, 1)
+ finally:
+ for router in pe_routers:
+ _configure_vrf_import(router, "BLUE", "GREEN", True)
+ _configure_skip_evpn_imported(router, "BLUE", True)
+ _configure_static_route(r1, "GREEN", VRF_IMPORT_PREFIX, False)
+
+
+def test_skip_evpn_imported_preserves_mixed_local_and_evpn_paths():
+ """Keep a local path while withdrawing EVPN-derived paths for the prefix."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+ pe_routers = (r2, r3)
+
+ try:
+ logger.info("Disable suppression to establish the mixed-path baseline")
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, False)
+
+ logger.info("Install the same prefix as an origin and a local BLUE route")
+ _configure_static_route(r1, "GREEN", MIXED_PATH_PREFIX, True)
+ _configure_static_route(r2, "BLUE", MIXED_PATH_PREFIX, True)
+ _wait_bgp_valid_path_count_at_least(r2, "BLUE", MIXED_PATH_PREFIX, 2)
+ _wait_evpn_type5_prefix_count_at_least(r1, MIXED_PATH_PREFIX, 2)
+ before_rds = set(evpn_type5_route_distinguishers(r1, MIXED_PATH_PREFIX))
+ assert (
+ len(before_rds) >= 2
+ ), "r1 {0}: expected at least 2 Type-5 RDs, actual={1}\n{2}".format(
+ MIXED_PATH_PREFIX,
+ before_rds,
+ r1.vtysh_cmd("show bgp l2vpn evpn route type 5"),
+ )
+
+ # The RD r2 exports its local BLUE route under. Suppression must leave
+ # this one advertised while the EVPN-derived copies are withdrawn.
+ blue_rd = _evpn_l3vni_rd(r2, BLUE_L3VNI)
+ assert (
+ blue_rd in before_rds
+ ), "r1 {0}: expected r2 BLUE export {1} in baseline {2}\n{3}".format(
+ MIXED_PATH_PREFIX,
+ blue_rd,
+ sorted(before_rds),
+ r1.vtysh_cmd("show bgp l2vpn evpn route type 5"),
+ )
+
+ logger.info("Suppress EVPN-derived paths while retaining the local path")
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+ _wait_bgp_route(r2, "BLUE", MIXED_PATH_PREFIX, True)
+ current_rds = set()
+
+ def _type5_exports_reduced():
+ nonlocal current_rds
+ current_rds = set(evpn_type5_route_distinguishers(r1, MIXED_PATH_PREFIX))
+ return current_rds < before_rds and blue_rd in current_rds
+
+ topotest.run_and_expect(_type5_exports_reduced, True, count=60, wait=1)
+ route_output = r2.vtysh_cmd(
+ "show bgp vrf BLUE ipv4 unicast {0}".format(MIXED_PATH_PREFIX)
+ )
+ type5_output = r1.vtysh_cmd("show bgp l2vpn evpn route type 5")
+ assert current_rds < before_rds, (
+ "r1 {0}: expected Type-5 RDs to be a proper subset of baseline; "
+ "before={1}, after={2}\n{3}".format(
+ MIXED_PATH_PREFIX,
+ sorted(before_rds),
+ sorted(current_rds),
+ type5_output,
+ )
+ )
+ # Shrinking alone would also be satisfied by wrongly dropping r2's own
+ # export, so require the RD carrying it to survive.
+ assert (
+ blue_rd in current_rds
+ ), "r1 {0}: expected r2 BLUE export {1} to survive; after={2}\n{3}".format(
+ MIXED_PATH_PREFIX,
+ blue_rd,
+ sorted(current_rds),
+ type5_output,
+ )
+ assert (
+ "weight 32768, valid" in route_output
+ ), "r2 BLUE {0}: expected valid local path with weight 32768\n{1}".format(
+ MIXED_PATH_PREFIX, route_output
+ )
+ finally:
+ _configure_static_route(r1, "GREEN", MIXED_PATH_PREFIX, False)
+ _configure_static_route(r2, "BLUE", MIXED_PATH_PREFIX, False)
+ for router in pe_routers:
+ for vrf in PE_VRFS:
+ _configure_skip_evpn_imported(router, vrf, True)
+
+
+def test_evpn_type5_vrf_leak_stale_after_origin_withdraw():
+ """Verify three origin cycles leave no stale VRF or EVPN Type-5 routes."""
+
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ r2 = tgen.gears["r2"]
+ r3 = tgen.gears["r3"]
+
+ try:
+ for cycle in range(3):
+ logger.info("Origin withdrawal/re-origination cycle {0}".format(cycle + 1))
+ logger.info("Inject the static origin route in r1 GREEN")
+ _configure_static_route(r1, "GREEN", PREFIX, True)
+
+ try:
+ logger.info("Verify both PEs import r1 EVPN-origin route into GREEN")
+ _wait_bgp_route(r1, "GREEN", PREFIX, True)
+ _wait_bgp_route(r2, "GREEN", PREFIX, True)
+ _wait_bgp_route(r3, "GREEN", PREFIX, True)
+
+ logger.info(
+ "Verify both PEs leak the EVPN-origin route into BLUE and PURPLE"
+ )
+ for router in (r2, r3):
+ _wait_bgp_route(router, "BLUE", PREFIX, True)
+ _wait_bgp_route(router, "PURPLE", PREFIX, True)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, PREFIX, 1)
+ except AssertionError:
+ _dump_route_state(
+ "EVPN-origin route before withdraw failure",
+ PREFIX,
+ ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
+ )
+ raise
+
+ if cycle == 0:
+ logger.info("Reload r2's existing unified configuration")
+ _reload_frr_config(r2, "/etc/frr/frr.conf")
+ result = verify_bgp_convergence_from_running_config(tgen, dut="r2")
+ assert result is True, "r2 BGP did not reconverge: {0}".format(result)
for vrf in PE_VRFS:
- _wait_bgp_route(router, vrf, PREFIX, False)
- except AssertionError:
- _dump_prefix_state(
- "EVPN-origin route after withdraw failure",
+ _wait_bgp_route(r2, vrf, PREFIX, True)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, PREFIX, 1)
+
+ logger.info("Dump EVPN-origin route before withdraw")
+ _dump_route_state(
+ "EVPN-origin route before withdraw",
+ PREFIX,
((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
)
- raise
- step("Dump EVPN-origin route after withdraw")
- _dump_prefix_state(
- "EVPN-origin route after withdraw",
- ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
- )
+ logger.info("Withdraw the origin prefix from r1 GREEN")
+ _configure_static_route(r1, "GREEN", PREFIX, False)
+
+ logger.info("Verify no stale leaked/re-exported copy remains on either PE")
+ try:
+ _wait_bgp_route(r1, "GREEN", PREFIX, False)
+ for router in (r2, r3):
+ for vrf in PE_VRFS:
+ _wait_bgp_route(router, vrf, PREFIX, False)
+ for router in (r1, r2, r3):
+ _wait_evpn_type5_prefix_count(router, PREFIX, 0)
+ except AssertionError:
+ _dump_route_state(
+ "EVPN-origin route after withdraw failure",
+ PREFIX,
+ ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
+ )
+ raise
+
+ logger.info("Dump EVPN-origin route after withdraw")
+ _dump_route_state(
+ "EVPN-origin route after withdraw",
+ PREFIX,
+ ((r1, ("GREEN",)), (r2, PE_VRFS), (r3, PE_VRFS)),
+ )
finally:
- _withdraw_origin_route()
+ _configure_static_route(r1, "GREEN", PREFIX, False)
def test_memory_leak():
diff --git a/tests/topotests/lib/evpn.py b/tests/topotests/lib/evpn.py
index 3e27c2a0e2..dfb1d1082f 100644
--- a/tests/topotests/lib/evpn.py
+++ b/tests/topotests/lib/evpn.py
@@ -999,6 +999,30 @@ def evpn_verify_route_advertisement(
return None
+def evpn_type5_prefix_count(router, prefix):
+ """Return the number of matching Type-5 routes in the EVPN BGP table."""
+ address, prefixlen = prefix.split("/", 1)
+ evpn_prefix = "[5]:[0]:[{0}]:[{1}]".format(prefixlen, address)
+ output = router.vtysh_cmd("show bgp l2vpn evpn route type 5")
+ return output.count(evpn_prefix)
+
+
+def evpn_type5_route_distinguishers(router, prefix):
+ """Return the route distinguisher headings carrying a Type-5 prefix."""
+ address, prefixlen = prefix.split("/", 1)
+ evpn_prefix = "[5]:[0]:[{0}]:[{1}]".format(prefixlen, address)
+ route_distinguishers = []
+ current_rd = None
+ output = router.vtysh_cmd("show bgp l2vpn evpn route type 5")
+ for line in output.splitlines():
+ stripped = line.strip()
+ if stripped.startswith("Route Distinguisher:"):
+ current_rd = stripped
+ elif evpn_prefix in line and current_rd:
+ route_distinguishers.append(current_rd)
+ return route_distinguishers
+
+
def evpn_verify_l3vni_nexthops(router, l3vni_list, expected_remote_vteps):
"""
Helper function to verify L3VNI EVPN next-hops from remote VTEPs.