@@ -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+
19582175def test_memory_leak (tgen_and_ip_version ):
19592176 """Run the memory leak test and report results."""
19602177 tgen = get_topogen ()
0 commit comments