From 8685f0a86cd5a13ea73b432b7da28e1c74f95374 Mon Sep 17 00:00:00 2001 From: Sergei Volkov Date: Tue, 12 May 2026 00:44:59 +0200 Subject: [PATCH 1/3] net, evpn: add VNI-aware teardown for L2 endpoints Clean up VLAN/VNI bridge mappings when removing L2 endpoints, allowing re-deployment with different parameters in the same session. Signed-off-by: Sergei Volkov Assisted-by: Claude Opus 4.6 --- tests/network/bgp/evpn/conftest.py | 2 +- tests/network/bgp/evpn/libevpn.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/network/bgp/evpn/conftest.py b/tests/network/bgp/evpn/conftest.py index 250aee4daf..8c54247127 100644 --- a/tests/network/bgp/evpn/conftest.py +++ b/tests/network/bgp/evpn/conftest.py @@ -235,7 +235,7 @@ def external_l2_endpoint( endpoint_ips=[EXTERNAL_L2_ENDPOINT_IPV4, EXTERNAL_L2_ENDPOINT_IPV6], ) yield endpoint - teardown_evpn_l2_endpoint(pod=frr_external_pod.pod) + teardown_evpn_l2_endpoint(pod=frr_external_pod.pod, vni=EVPN_MAC_VRF_VNI) @pytest.fixture(scope="module") diff --git a/tests/network/bgp/evpn/libevpn.py b/tests/network/bgp/evpn/libevpn.py index 709d3258f0..5dcbe4720c 100644 --- a/tests/network/bgp/evpn/libevpn.py +++ b/tests/network/bgp/evpn/libevpn.py @@ -169,11 +169,20 @@ def deploy_evpn_l2_endpoint( return EvpnEndpoint(pod=pod, ip_addresses=bare_ips, netns_name=_L2_ENDPOINT_NETNS) -def teardown_evpn_l2_endpoint(pod: Pod) -> None: - """Removes the EVPN L2 endpoint (netns, veth) from the FRR pod.""" +def teardown_evpn_l2_endpoint(pod: Pod, vni: int) -> None: + """Removes the EVPN L2 endpoint (netns, veth, VLAN/VNI mappings) from the FRR pod. + + Args: + pod: The FRR pod hosting the endpoint. + vni: MAC-VRF VNI used during deployment. + """ for cmd in [ f"ip netns delete {_L2_ENDPOINT_NETNS}", f"ip link delete {_L2_VETH_POD_SIDE}", + f"bridge vlan del dev {_VXLAN_NAME} vid {_L2_VID} tunnel_info id {vni}", + f"bridge vni del dev {_VXLAN_NAME} vni {vni}", + f"bridge vlan del dev {_VXLAN_NAME} vid {_L2_VID}", + f"bridge vlan del dev {_BRIDGE_NAME} vid {_L2_VID} self", ]: pod.execute(command=shlex.split(cmd), container=NET_TOOLS_CONTAINER_NAME, ignore_rc=True) From 7483cce650b641153136cede383b9bb8229801c0 Mon Sep 17 00:00:00 2001 From: Sergei Volkov Date: Tue, 12 May 2026 00:50:03 +0200 Subject: [PATCH 2/3] net, evpn: add source provider migration test Emulates migration of an external L2 workload into the cluster as a CUDN VM, preserving its IP and MAC addresses. The old endpoint is torn down and re-created with new IPs while the VM takes over the original identity. test_source_provider_migration uses @pytest.mark.order("last") because it tears down the shared L2 endpoint, which would break other tests if run earlier. Signed-off-by: Sergei Volkov Assisted-by: Claude Opus 4.6 Signed-off-by: Sergei Volkov --- libs/vm/spec.py | 1 + tests/network/bgp/evpn/conftest.py | 39 ++++++++++++++++-- tests/network/bgp/evpn/libevpn.py | 15 +++++-- .../bgp/evpn/test_evpn_connectivity.py | 41 +++++++++++++++++-- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/libs/vm/spec.py b/libs/vm/spec.py index 75f5f9a973..c11a21a9d5 100644 --- a/libs/vm/spec.py +++ b/libs/vm/spec.py @@ -77,6 +77,7 @@ class Interface: passtBinding: dict[Any, Any] | None = None # noqa: N815 binding: NetBinding | None = None state: str | None = None + macAddress: str | None = None # noqa: N815 @dataclass diff --git a/tests/network/bgp/evpn/conftest.py b/tests/network/bgp/evpn/conftest.py index 8c54247127..dd7085e8ec 100644 --- a/tests/network/bgp/evpn/conftest.py +++ b/tests/network/bgp/evpn/conftest.py @@ -11,11 +11,15 @@ from ocp_resources.resource import ResourceEditor from ocp_resources.vtep import VTEP -from libs.net.ip import random_ipv4_address, random_ipv6_address +from libs.net.ip import random_ipv4_address from libs.net.traffic_generator import TcpServer -from libs.net.udn import UDN_BINDING_DEFAULT_PLUGIN_NAME, create_udn_namespace +from libs.net.udn import UDN_BINDING_DEFAULT_PLUGIN_NAME, create_udn_namespace, udn_primary_network +from libs.vm.affinity import new_pod_anti_affinity +from libs.vm.factory import base_vmspec, fedora_vm +from libs.vm.spec import Devices, Metadata from libs.vm.vm import BaseVirtualMachine from tests.network.bgp.evpn.libevpn import ( + CUDN_EVPN_SUBNET_IPV6, EndpointTcpClient, EvpnEndpoint, cudn_evpn_subnets, @@ -42,7 +46,8 @@ APP_EVPN_CUDN_LABEL: Final[dict] = {**EVPN_ADVERTISE_LABEL, "app": "cudn-evpn"} CUDN_EVPN_BGP_LABEL: Final[dict] = {"cudn-bgp": "evpn"} EXTERNAL_L2_ENDPOINT_IPV4: Final[str] = f"{random_ipv4_address(net_seed=5, host_address=250)}/24" -EXTERNAL_L2_ENDPOINT_IPV6: Final[str] = f"{random_ipv6_address(net_seed=5, host_address=250)}/64" +EXTERNAL_L2_ENDPOINT_IPV6: Final[str] = f"{ipaddress.ip_network(CUDN_EVPN_SUBNET_IPV6, strict=False)[250]}/64" +EXTERNAL_L2_ENDPOINT_MAC: Final[str] = "02:00:05:00:fa:00" EXTERNAL_L3_ENDPOINT_IPV4: Final[str] = "192.168.100.100/24" EXTERNAL_L3_ENDPOINT_IPV6: Final[str] = "fd01:1234:5678::64/64" EXTERNAL_L3_GATEWAY_IPV4: Final[str] = "192.168.100.1/24" @@ -233,6 +238,7 @@ def external_l2_endpoint( pod=frr_external_pod.pod, vni=EVPN_MAC_VRF_VNI, endpoint_ips=[EXTERNAL_L2_ENDPOINT_IPV4, EXTERNAL_L2_ENDPOINT_IPV6], + mac_address=EXTERNAL_L2_ENDPOINT_MAC, ) yield endpoint teardown_evpn_l2_endpoint(pod=frr_external_pod.pod, vni=EVPN_MAC_VRF_VNI) @@ -269,3 +275,30 @@ def evpn_routed_l3_active_connections( ) -> Generator[list[tuple[EndpointTcpClient, TcpServer]]]: with evpn_workloads_active_connections(endpoint=external_l3_endpoint, vm=vm_evpn_target) as connections: yield connections + + +@pytest.fixture() +def vm_source_provider( + external_l2_endpoint: EvpnEndpoint, + namespace_evpn: Namespace, + cudn_evpn_layer2: libcudn.ClusterUserDefinedNetwork, + admin_client: DynamicClient, + frr_external_pod: ExternalFrrPodInfo, +) -> Generator[BaseVirtualMachine]: + spec = base_vmspec() + network_name = "udn-network" + iface, network = udn_primary_network(name=network_name, binding=UDN_BINDING_DEFAULT_PLUGIN_NAME) + iface.macAddress = external_l2_endpoint.mac_address + spec.template.spec.domain.devices = Devices(interfaces=[iface]) + spec.template.spec.networks = [network] + spec.template.metadata = Metadata( + labels=EXTERNAL_FRR_POD_LABEL, + annotations={"network.kubevirt.io/addresses": json.dumps({network_name: external_l2_endpoint.ip_addresses})}, + ) + label, *_ = EXTERNAL_FRR_POD_LABEL.items() + spec.template.spec.affinity = new_pod_anti_affinity( + label=label, namespaces=[frr_external_pod.pod.namespace, namespace_evpn.name] + ) + + with fedora_vm(namespace=namespace_evpn.name, name="vm-source-provider", client=admin_client, spec=spec) as vm: + yield vm diff --git a/tests/network/bgp/evpn/libevpn.py b/tests/network/bgp/evpn/libevpn.py index 5dcbe4720c..53d8f45454 100644 --- a/tests/network/bgp/evpn/libevpn.py +++ b/tests/network/bgp/evpn/libevpn.py @@ -51,6 +51,7 @@ class EvpnEndpoint: pod: Pod ip_addresses: list[str] netns_name: str + mac_address: str | None = None class EndpointTcpClient(PodTcpClient): @@ -143,6 +144,7 @@ def deploy_evpn_l2_endpoint( pod: Pod, vni: int, endpoint_ips: list[str], + mac_address: str | None = None, ) -> EvpnEndpoint: """Creates a stretched L2 endpoint on the shared SVD bridge. @@ -155,18 +157,19 @@ def deploy_evpn_l2_endpoint( pod: The FRR pod hosting the endpoint. vni: MAC-VRF VNI (must match CUDN's macVRF VNI). endpoint_ips: IPs with prefix length (e.g. ["10.0.5.250/24", "fd00::fa/64"]). + mac_address: Explicit MAC for the endpoint interface (locally-administered). Returns: EvpnEndpoint. """ - commands = _build_l2_endpoint_commands(vni=vni, endpoint_ips=endpoint_ips) + commands = _build_l2_endpoint_commands(vni=vni, endpoint_ips=endpoint_ips, mac_address=mac_address) for command in commands: pod.execute(command=shlex.split(command), container=NET_TOOLS_CONTAINER_NAME) bare_ips = [ip.split("/")[0] for ip in endpoint_ips] LOGGER.info(f"EVPN L2 endpoint deployed: {bare_ips} in namespace {_L2_ENDPOINT_NETNS}") - return EvpnEndpoint(pod=pod, ip_addresses=bare_ips, netns_name=_L2_ENDPOINT_NETNS) + return EvpnEndpoint(pod=pod, ip_addresses=bare_ips, netns_name=_L2_ENDPOINT_NETNS, mac_address=mac_address) def teardown_evpn_l2_endpoint(pod: Pod, vni: int) -> None: @@ -189,7 +192,12 @@ def teardown_evpn_l2_endpoint(pod: Pod, vni: int) -> None: LOGGER.info(f"EVPN L2 endpoint removed: namespace={_L2_ENDPOINT_NETNS}") -def _build_l2_endpoint_commands(vni: int, endpoint_ips: list[str]) -> list[str]: +def _build_l2_endpoint_commands( + vni: int, + endpoint_ips: list[str], + mac_address: str | None = None, +) -> list[str]: + mac_cmd = [f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set dev {_L2_VETH_EP_SIDE} address {mac_address}"] return [ f"bridge vlan add dev {_BRIDGE_NAME} vid {_L2_VID} self", f"bridge vlan add dev {_VXLAN_NAME} vid {_L2_VID}", @@ -202,6 +210,7 @@ def _build_l2_endpoint_commands(vni: int, endpoint_ips: list[str]) -> list[str]: f"ip netns add {_L2_ENDPOINT_NETNS}", f"ip link set {_L2_VETH_EP_SIDE} netns {_L2_ENDPOINT_NETNS}", *(f"ip netns exec {_L2_ENDPOINT_NETNS} ip addr add {ip} dev {_L2_VETH_EP_SIDE}" for ip in endpoint_ips), + *(mac_cmd if mac_address else []), f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set {_L2_VETH_EP_SIDE} up", f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set lo up", ] diff --git a/tests/network/bgp/evpn/test_evpn_connectivity.py b/tests/network/bgp/evpn/test_evpn_connectivity.py index 301ffc1440..d2b46f2f13 100644 --- a/tests/network/bgp/evpn/test_evpn_connectivity.py +++ b/tests/network/bgp/evpn/test_evpn_connectivity.py @@ -20,11 +20,20 @@ import pytest +from libs.net.ip import random_ipv4_address, random_ipv6_address from libs.net.traffic_generator import active_tcp_connections, is_tcp_connection from libs.net.vmspec import lookup_primary_network -from tests.network.bgp.evpn.libevpn import assert_evpn_workloads_connectivity, evpn_workloads_active_connections +from tests.network.bgp.evpn.libevpn import ( + assert_evpn_workloads_connectivity, + deploy_evpn_l2_endpoint, + evpn_workloads_active_connections, + teardown_evpn_l2_endpoint, +) from utilities.virt import migrate_vm_and_verify +_L2_ENDPOINT_IPV4: str = f"{random_ipv4_address(net_seed=5, host_address=249)}/24" +_L2_ENDPOINT_IPV6: str = f"{random_ipv6_address(net_seed=5, host_address=249)}/64" + pytestmark = [ pytest.mark.bgp, pytest.mark.ipv4, @@ -176,7 +185,15 @@ def test_connectivity_after_udn_vm_cold_reboot( @pytest.mark.polarion("CNV-15233") -def test_source_provider_migration(): +@pytest.mark.order("last") +def test_source_provider_migration( + external_l3_endpoint, + cudn_evpn_layer2, + vm_source_provider, + vm_evpn_target, + frr_external_pod, + subtests, +): """ Scenario emulates a migration of an external workload (Source Provider) into the OCP cluster as a CUDN VM, while preserving its IP and MAC addresses, and maintaining connectivity. @@ -185,6 +202,7 @@ def test_source_provider_migration(): - External Source Provider L2 and L3 endpoints. - Running connectivity reference VM with a primary EVPN-enabled CUDN. - TCP connectivity exists between the connectivity reference VM and the external L2 and L3 endpoints. + Precondition is verified in preceding tests. Steps: 1. Shut down/remove the external L2 endpoint. @@ -194,6 +212,23 @@ def test_source_provider_migration(): Expected: - New connections are established after new UDN VM deployment. """ + mac_vrf_vni = cudn_evpn_layer2.instance.spec.network.evpn.macVRF.vni + + teardown_evpn_l2_endpoint(pod=frr_external_pod.pod, vni=mac_vrf_vni) + vm_source_provider.start(wait=True) + vm_source_provider.wait_for_agent_connected() -test_source_provider_migration.__test__ = False + new_l2_endpoint = deploy_evpn_l2_endpoint( + pod=frr_external_pod.pod, + vni=mac_vrf_vni, + endpoint_ips=[_L2_ENDPOINT_IPV4, _L2_ENDPOINT_IPV6], + ) + + assert_evpn_workloads_connectivity( + target_vm=vm_evpn_target, + ref_vm=vm_source_provider, + l2_endpoint=new_l2_endpoint, + l3_endpoint=external_l3_endpoint, + subtests=subtests, + ) From b827d09f389303c6c3fed89351556d674225fe66 Mon Sep 17 00:00:00 2001 From: Sergei Volkov Date: Tue, 12 May 2026 00:50:03 +0200 Subject: [PATCH 3/3] net, evpn: add source provider migration test Emulates migration of an external L2 workload into the cluster as a CUDN VM, preserving its IP and MAC addresses. The old endpoint is torn down and re-created with new IPs while the VM takes over the original identity. test_source_provider_migration uses @pytest.mark.order("last") because it tears down the shared L2 endpoint, which would break other tests if run earlier. Signed-off-by: Sergei Volkov Assisted-by: Claude Opus 4.6 --- tests/network/bgp/evpn/conftest.py | 3 ++- tests/network/bgp/evpn/libevpn.py | 12 ++++++++---- tests/network/bgp/evpn/test_evpn_connectivity.py | 5 +++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/network/bgp/evpn/conftest.py b/tests/network/bgp/evpn/conftest.py index dd7085e8ec..35bb94a9e2 100644 --- a/tests/network/bgp/evpn/conftest.py +++ b/tests/network/bgp/evpn/conftest.py @@ -20,6 +20,7 @@ from libs.vm.vm import BaseVirtualMachine from tests.network.bgp.evpn.libevpn import ( CUDN_EVPN_SUBNET_IPV6, + EVPN_CUDN_NET_SEED, EndpointTcpClient, EvpnEndpoint, cudn_evpn_subnets, @@ -45,7 +46,7 @@ EVPN_ADVERTISE_LABEL: Final[dict] = {"advertise": "evpn"} APP_EVPN_CUDN_LABEL: Final[dict] = {**EVPN_ADVERTISE_LABEL, "app": "cudn-evpn"} CUDN_EVPN_BGP_LABEL: Final[dict] = {"cudn-bgp": "evpn"} -EXTERNAL_L2_ENDPOINT_IPV4: Final[str] = f"{random_ipv4_address(net_seed=5, host_address=250)}/24" +EXTERNAL_L2_ENDPOINT_IPV4: Final[str] = f"{random_ipv4_address(net_seed=EVPN_CUDN_NET_SEED, host_address=250)}/24" EXTERNAL_L2_ENDPOINT_IPV6: Final[str] = f"{ipaddress.ip_network(CUDN_EVPN_SUBNET_IPV6, strict=False)[250]}/64" EXTERNAL_L2_ENDPOINT_MAC: Final[str] = "02:00:05:00:fa:00" EXTERNAL_L3_ENDPOINT_IPV4: Final[str] = "192.168.100.100/24" diff --git a/tests/network/bgp/evpn/libevpn.py b/tests/network/bgp/evpn/libevpn.py index 53d8f45454..dab2bbdb9c 100644 --- a/tests/network/bgp/evpn/libevpn.py +++ b/tests/network/bgp/evpn/libevpn.py @@ -24,8 +24,9 @@ LOGGER = logging.getLogger(__name__) -CUDN_EVPN_SUBNET_IPV4: str = f"{random_ipv4_address(net_seed=5, host_address=0)}/24" -CUDN_EVPN_SUBNET_IPV6: str = f"{random_ipv6_address(net_seed=5, host_address=0)}/64" +EVPN_CUDN_NET_SEED: int = 5 +CUDN_EVPN_SUBNET_IPV4: str = f"{random_ipv4_address(net_seed=EVPN_CUDN_NET_SEED, host_address=0)}/24" +CUDN_EVPN_SUBNET_IPV6: str = f"{random_ipv6_address(net_seed=EVPN_CUDN_NET_SEED, host_address=0)}/64" _BRIDGE_NAME: str = "br0" _VXLAN_NAME: str = "vxlan0" @@ -197,7 +198,6 @@ def _build_l2_endpoint_commands( endpoint_ips: list[str], mac_address: str | None = None, ) -> list[str]: - mac_cmd = [f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set dev {_L2_VETH_EP_SIDE} address {mac_address}"] return [ f"bridge vlan add dev {_BRIDGE_NAME} vid {_L2_VID} self", f"bridge vlan add dev {_VXLAN_NAME} vid {_L2_VID}", @@ -210,7 +210,11 @@ def _build_l2_endpoint_commands( f"ip netns add {_L2_ENDPOINT_NETNS}", f"ip link set {_L2_VETH_EP_SIDE} netns {_L2_ENDPOINT_NETNS}", *(f"ip netns exec {_L2_ENDPOINT_NETNS} ip addr add {ip} dev {_L2_VETH_EP_SIDE}" for ip in endpoint_ips), - *(mac_cmd if mac_address else []), + *( + [f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set dev {_L2_VETH_EP_SIDE} address {mac_address}"] + if mac_address + else [] + ), f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set {_L2_VETH_EP_SIDE} up", f"ip netns exec {_L2_ENDPOINT_NETNS} ip link set lo up", ] diff --git a/tests/network/bgp/evpn/test_evpn_connectivity.py b/tests/network/bgp/evpn/test_evpn_connectivity.py index d2b46f2f13..2e71748f4c 100644 --- a/tests/network/bgp/evpn/test_evpn_connectivity.py +++ b/tests/network/bgp/evpn/test_evpn_connectivity.py @@ -24,6 +24,7 @@ from libs.net.traffic_generator import active_tcp_connections, is_tcp_connection from libs.net.vmspec import lookup_primary_network from tests.network.bgp.evpn.libevpn import ( + EVPN_CUDN_NET_SEED, assert_evpn_workloads_connectivity, deploy_evpn_l2_endpoint, evpn_workloads_active_connections, @@ -31,8 +32,8 @@ ) from utilities.virt import migrate_vm_and_verify -_L2_ENDPOINT_IPV4: str = f"{random_ipv4_address(net_seed=5, host_address=249)}/24" -_L2_ENDPOINT_IPV6: str = f"{random_ipv6_address(net_seed=5, host_address=249)}/64" +_L2_ENDPOINT_IPV4: str = f"{random_ipv4_address(net_seed=EVPN_CUDN_NET_SEED, host_address=249)}/24" +_L2_ENDPOINT_IPV6: str = f"{random_ipv6_address(net_seed=EVPN_CUDN_NET_SEED, host_address=249)}/64" pytestmark = [ pytest.mark.bgp,