Skip to content

Commit 292e194

Browse files
committed
tests: EVPN validate l3vni to l2vni transition
Signed-off-by: Chirag Shah <[email protected]>
1 parent 3b2930c commit 292e194

File tree

2 files changed

+256
-23
lines changed

2 files changed

+256
-23
lines changed

tests/topotests/bgp_evpn_three_tier_clos_topo1/test_bgp_evpn_v4_v6_vtep.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,223 @@ def test_for_leaked_route_as_path(tgen_and_ip_version):
19551955
logger.error(f"Common cleanup failed: {e}")
19561956

19571957

1958+
def test_l3vni_l2vni_transition_restore(tgen_and_ip_version):
1959+
"""
1960+
Verify L3VNI -> L2VNI transition and restoration back to L3VNI.
1961+
1962+
Based on the zebra fix for L3VNI to L2VNI transition, this test performs:
1963+
1. Capture current L3VNI state for VNI 104001 on bordertor-11
1964+
(type, VLAN, bridge, tenant VRF, router MAC, local/remote VTEPs).
1965+
2. Remove the L3VNI binding from VRF1 (no vni 104001 under vrf vrf1) and
1966+
verify that:
1967+
- The VNI still exists in zebra.
1968+
- The VNI type changes to L2.
1969+
- VLAN / bridge / remoteVteps remain populated.
1970+
3. Re‑add the L3VNI binding (vni 104001 under vrf vrf1) and verify that:
1971+
- The VNI type returns to L3.
1972+
- tenant VRF, VLAN, bridge, router MAC, local/remote VTEPs match the
1973+
original L3VNI state captured in step 1.
1974+
"""
1975+
tgen, ip_version = tgen_and_ip_version
1976+
if tgen.routers_have_failure():
1977+
pytest.skip(tgen.errors)
1978+
1979+
logger.info(
1980+
"Testing L3VNI -> L2VNI transition and restore on bordertor-11 "
1981+
f"({ip_version} underlay)"
1982+
)
1983+
1984+
router = tgen.gears["bordertor-11"]
1985+
l3vni = "104001"
1986+
1987+
# Ensure initial L3VNI state is fully up and validated using common helper.
1988+
test_func = partial(evpn_verify_vni_state, router, [l3vni], vni_type="L3")
1989+
_, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
1990+
assert (
1991+
result is None
1992+
), f"Initial L3VNI state verification failed for VNI {l3vni}: {result}"
1993+
1994+
# Capture initial EVPN VNI JSON for L3VNI.
1995+
initial = router.vtysh_cmd(f"show evpn vni {l3vni} json", isjson=True)
1996+
assert initial, f"No EVPN VNI JSON output for VNI {l3vni} before transition"
1997+
1998+
assert (
1999+
initial.get("type") == "L3"
2000+
), f"Expected VNI {l3vni} type=L3 before transition, got {initial.get('type')}"
2001+
2002+
initial_vrf = initial.get("tenantVrf", initial.get("vrf"))
2003+
initial_vlan = initial.get("vlan")
2004+
initial_bridge = initial.get("bridge")
2005+
initial_router_mac = initial.get("routerMac")
2006+
initial_local_vtep = initial.get("localVtep")
2007+
initial_num_remote = initial.get("numRemoteVteps", 0)
2008+
initial_remote_ips = sorted(
2009+
vtep.get("ip")
2010+
for vtep in initial.get("remoteVteps", [])
2011+
if isinstance(vtep, dict) and "ip" in vtep
2012+
)
2013+
2014+
logger.info(
2015+
"Initial L3VNI %s on %s: vrf=%s vlan=%s bridge=%s routerMac=%s "
2016+
"localVtep=%s numRemoteVteps=%s remoteVteps=%s",
2017+
l3vni,
2018+
router.name,
2019+
initial_vrf,
2020+
initial_vlan,
2021+
initial_bridge,
2022+
initial_router_mac,
2023+
initial_local_vtep,
2024+
initial_num_remote,
2025+
initial_remote_ips,
2026+
)
2027+
2028+
# Step 2: Remove L3VNI binding from VRF1 (L3VNI -> L2VNI transition).
2029+
logger.info("Removing L3VNI binding (no vni %s under vrf vrf1)", l3vni)
2030+
router.vtysh_multicmd(
2031+
f"""
2032+
configure terminal
2033+
vrf vrf1
2034+
no vni {l3vni}
2035+
exit
2036+
exit
2037+
"""
2038+
)
2039+
2040+
def _check_l3vni_to_l2vni():
2041+
# First, use the generic helper to validate basic L2VNI fields.
2042+
res = evpn_verify_vni_state(router, [l3vni], vni_type="L2")
2043+
if res is not None:
2044+
return f"L3->L2 transition basic validation failed: {res}"
2045+
2046+
out = router.vtysh_cmd(f"show evpn vni {l3vni} json", isjson=True)
2047+
if not out:
2048+
return f"VNI {l3vni}: no EVPN VNI JSON after removing L3VNI binding"
2049+
2050+
# VLAN should still be present and unchanged.
2051+
if out.get("vlan") != initial_vlan:
2052+
return (
2053+
f"VNI {l3vni}: vlan changed across L3->L2 transition, "
2054+
f"expected {initial_vlan}, found {out.get('vlan')}"
2055+
)
2056+
2057+
# Remote VTEPs handling:
2058+
# On some platforms, an L3VNI->L2VNI transition can leave
2059+
# numRemoteVteps at 0 (no remote VTEPs learned yet) and omit the
2060+
# detailed remoteVteps list entirely. Treat that as a valid transient
2061+
# state and only enforce the list when the count is non-zero.
2062+
num_remote = out.get("numRemoteVteps", 0)
2063+
if num_remote == 0:
2064+
logger.info(
2065+
"VNI %s: numRemoteVteps is 0 after L3->L2 transition (no remote VTEPs yet)",
2066+
l3vni,
2067+
)
2068+
else:
2069+
rem_ips = sorted(
2070+
vtep.get("ip")
2071+
for vtep in out.get("remoteVteps", [])
2072+
if isinstance(vtep, dict) and "ip" in vtep
2073+
)
2074+
if not rem_ips:
2075+
return (
2076+
f"VNI {l3vni}: remoteVteps empty after L3->L2 transition "
2077+
f"(expected {num_remote})"
2078+
)
2079+
2080+
return None
2081+
2082+
_, result = topotest.run_and_expect(_check_l3vni_to_l2vni, None, count=30, wait=1)
2083+
assert (
2084+
result is None
2085+
), f"L3VNI -> L2VNI transition validation failed for VNI {l3vni}: {result}"
2086+
2087+
# Step 3: Re‑add L3VNI binding and ensure full restoration.
2088+
logger.info("Re‑adding L3VNI binding (vni %s under vrf vrf1)", l3vni)
2089+
router.vtysh_multicmd(
2090+
f"""
2091+
configure terminal
2092+
vrf vrf1
2093+
vni {l3vni}
2094+
exit
2095+
exit
2096+
"""
2097+
)
2098+
2099+
def _check_l3vni_restored():
2100+
# Re‑use common helper to ensure L3VNI view is healthy first.
2101+
res = evpn_verify_vni_state(router, [l3vni], vni_type="L3")
2102+
if res is not None:
2103+
return f"L3VNI restore basic validation failed: {res}"
2104+
2105+
out = router.vtysh_cmd(f"show evpn vni {l3vni} json", isjson=True)
2106+
if not out:
2107+
return f"VNI {l3vni}: no EVPN VNI JSON after re‑adding L3VNI binding"
2108+
2109+
vrf_name = out.get("tenantVrf", out.get("vrf"))
2110+
if vrf_name != initial_vrf:
2111+
return (
2112+
f"VNI {l3vni}: tenant VRF mismatch after restore, "
2113+
f"expected {initial_vrf}, found {vrf_name}"
2114+
)
2115+
2116+
if out.get("vlan") != initial_vlan:
2117+
return (
2118+
f"VNI {l3vni}: vlan mismatch after restore, "
2119+
f"expected {initial_vlan}, found {out.get('vlan')}"
2120+
)
2121+
2122+
if out.get("bridge") != initial_bridge:
2123+
return (
2124+
f"VNI {l3vni}: bridge mismatch after restore, "
2125+
f"expected {initial_bridge}, found {out.get('bridge')}"
2126+
)
2127+
2128+
if initial_router_mac and out.get("routerMac") != initial_router_mac:
2129+
return (
2130+
f"VNI {l3vni}: routerMac mismatch after restore, "
2131+
f"expected {initial_router_mac}, found {out.get('routerMac')}"
2132+
)
2133+
2134+
if initial_local_vtep and out.get("localVtep") != initial_local_vtep:
2135+
return (
2136+
f"VNI {l3vni}: localVtep mismatch after restore, "
2137+
f"expected {initial_local_vtep}, found {out.get('localVtep')}"
2138+
)
2139+
2140+
# Remote VTEPs and count should match pre‑transition state (order‑insensitive).
2141+
rem_ips = sorted(
2142+
vtep.get("ip")
2143+
for vtep in out.get("remoteVteps", [])
2144+
if isinstance(vtep, dict) and "ip" in vtep
2145+
)
2146+
if initial_remote_ips and rem_ips != initial_remote_ips:
2147+
return (
2148+
f"VNI {l3vni}: remoteVteps mismatch after restore, "
2149+
f"expected {initial_remote_ips}, found {rem_ips}"
2150+
)
2151+
2152+
num_remote = out.get("numRemoteVteps", 0)
2153+
if initial_num_remote and num_remote != initial_num_remote:
2154+
return (
2155+
f"VNI {l3vni}: numRemoteVteps mismatch after restore, "
2156+
f"expected {initial_num_remote}, found {num_remote}"
2157+
)
2158+
2159+
return None
2160+
2161+
_, result = topotest.run_and_expect(_check_l3vni_restored, None, count=60, wait=1)
2162+
assert (
2163+
result is None
2164+
), f"L3VNI restoration validation failed for VNI {l3vni}: {result}"
2165+
2166+
# Additionally confirm BGP control-plane view for the L3VNI using the
2167+
# common EVPN helper.
2168+
test_func = partial(evpn_verify_bgp_vni_state, router, [l3vni])
2169+
_, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
2170+
assert (
2171+
result is None
2172+
), f"BGP L3VNI state validation failed for VNI {l3vni} after restore: {result}"
2173+
2174+
19582175
def test_memory_leak(tgen_and_ip_version):
19592176
"""Run the memory leak test and report results."""
19602177
tgen = get_topogen()

tests/topotests/lib/evpn.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -406,42 +406,58 @@ def evpn_verify_vni_state(router, vni_list, vni_type="L2", expected_state="Up"):
406406
if vni_type_field is not None and vni_type_field != "L2":
407407
return f"VNI {vni}: Expected L2 VNI but found type: {vni_type_field}"
408408

409-
# Check remoteVteps field exists
410-
if "remoteVteps" not in output:
411-
return f"VNI {vni}: 'remoteVteps' field not found in output"
412-
413-
# Check numRemoteVteps field exists and is valid
409+
# Check numRemoteVteps field exists first. Some FRR versions omit
410+
# the 'remoteVteps' array entirely when the count is zero, so we
411+
# must look at the counter before assuming the array will be
412+
# present.
414413
if "numRemoteVteps" not in output:
415414
return f"VNI {vni}: 'numRemoteVteps' field not found in output"
416415

417416
num_remote_vteps = output.get("numRemoteVteps", 0)
418-
remote_vteps = output.get("remoteVteps", [])
419-
actual_remote_vtep_count = len(remote_vteps)
420417

421-
# Verify numRemoteVteps matches actual count
422-
if num_remote_vteps != actual_remote_vtep_count:
423-
return (
424-
f"VNI {vni}: numRemoteVteps mismatch. "
425-
f"Field says {num_remote_vteps}, but found {actual_remote_vtep_count} entries"
418+
# If there are zero remote VTEPs, that's valid – just log and skip
419+
# the detailed remoteVteps checks.
420+
if num_remote_vteps == 0:
421+
logger.info(
422+
"%s: VNI %s (L2) currently has 0 remote VTEPs",
423+
router.name,
424+
vni,
426425
)
426+
else:
427+
# When numRemoteVteps is non-zero, the detailed 'remoteVteps'
428+
# list must be present and consistent with the counter.
429+
if "remoteVteps" not in output:
430+
return f"VNI {vni}: 'remoteVteps' field not found in output"
427431

428-
logger.info(
429-
"%s: VNI %s (L2) has %s remote VTEPs",
430-
router.name,
431-
vni,
432-
num_remote_vteps,
433-
)
432+
remote_vteps = output.get("remoteVteps", [])
433+
actual_remote_vtep_count = len(remote_vteps)
434+
435+
# Verify numRemoteVteps matches actual count
436+
if num_remote_vteps != actual_remote_vtep_count:
437+
return (
438+
f"VNI {vni}: numRemoteVteps mismatch. "
439+
f"Field says {num_remote_vteps}, but found {actual_remote_vtep_count} entries"
440+
)
434441

435-
# Log remote VTEP IPs if available
436-
if remote_vteps:
437-
remote_vtep_ips = [vtep.get("ip", "unknown") for vtep in remote_vteps]
438442
logger.info(
439-
"%s: VNI %s remote VTEPs: %s",
443+
"%s: VNI %s (L2) has %s remote VTEPs",
440444
router.name,
441445
vni,
442-
remote_vtep_ips,
446+
num_remote_vteps,
443447
)
444448

449+
# Log remote VTEP IPs if available
450+
if remote_vteps:
451+
remote_vtep_ips = [
452+
vtep.get("ip", "unknown") for vtep in remote_vteps
453+
]
454+
logger.info(
455+
"%s: VNI %s remote VTEPs: %s",
456+
router.name,
457+
vni,
458+
remote_vtep_ips,
459+
)
460+
445461
# Validate VLAN and bridge for L2VNIs as well. These come from
446462
# zebra_evpn's JSON ("vlan", "bridge").
447463
if "vlan" not in output:

0 commit comments

Comments
 (0)