-
Notifications
You must be signed in to change notification settings - Fork 68
net, evpn: add routed L3 connectivity tests #4776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,13 +5,14 @@ | |
| from dataclasses import dataclass | ||
|
|
||
| from ocp_resources.pod import Pod | ||
| from timeout_sampler import retry | ||
|
|
||
| from libs.net.cluster import ipv4_supported_cluster, ipv6_supported_cluster | ||
| from libs.net.ip import filter_link_local_addresses, random_ipv4_address, random_ipv6_address | ||
| from libs.net.traffic_generator import IPERF_SERVER_PORT, PodTcpClient, TcpServer | ||
| from libs.net.vmspec import lookup_iface_status, lookup_primary_network | ||
| from libs.vm.vm import BaseVirtualMachine | ||
| from tests.network.libs.bgp import NET_TOOLS_CONTAINER_NAME | ||
| from tests.network.libs.bgp import CLUSTER_FRR_ASN, EXTERNAL_FRR_ASN, NET_TOOLS_CONTAINER_NAME | ||
|
|
||
| LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -27,6 +28,13 @@ | |
| _L2_VETH_POD_SIDE: str = "veth-l2-frr" | ||
| _L2_VETH_EP_SIDE: str = "veth-l2-ep" | ||
|
|
||
| _L3_VID: int = 200 | ||
| _L3_VRF_NAME: str = "vrf-blue" | ||
| _L3_SVI_NAME: str = f"{_BRIDGE_NAME}.{_L3_VID}" | ||
| _L3_ENDPOINT_NETNS: str = "l3-ep" | ||
| _L3_VETH_POD_SIDE: str = "veth-l3-frr" | ||
| _L3_VETH_EP_SIDE: str = "veth-l3-ep" | ||
|
|
||
|
|
||
| @dataclass | ||
| class EvpnEndpoint: | ||
|
|
@@ -182,6 +190,129 @@ def _build_l2_endpoint_commands(vni: int, endpoint_ips: list[str]) -> list[str]: | |
| ] | ||
|
|
||
|
|
||
| def deploy_evpn_l3_endpoint( | ||
| pod: Pod, | ||
| vni: int, | ||
| endpoint_ips: list[str], | ||
| gateway_ips: list[str], | ||
| ) -> EvpnEndpoint: | ||
| """Creates a routed L3 endpoint on the external FRR pod. | ||
|
|
||
| Deploys Linux infra (VRF, SVI, VLAN/VNI, veth, netns), configures | ||
| external FRR BGP VRF for Type-5 routes, and waits for OVN-K routes. | ||
|
|
||
| Data path: VM -> OVN L3 lookup -> VXLAN (IP-VRF VNI) -> vxlan0 -> br0 -> SVI -> VRF -> veth -> l3-ep. | ||
|
|
||
| Args: | ||
| pod: The external FRR pod. | ||
| vni: IP-VRF VNI (must match UDN's ipVRF VNI). | ||
| endpoint_ips: IPs with prefix on a different subnet than CUDN (e.g. ["192.168.100.100/24"]). | ||
| gateway_ips: Gateway IPs with prefix for the VRF veth side (e.g. ["192.168.100.1/24"]). | ||
|
|
||
| Returns: | ||
| EvpnEndpoint. | ||
| """ | ||
| commands = _build_l3_endpoint_commands(vni=vni, endpoint_ips=endpoint_ips, gateway_ips=gateway_ips) | ||
| for command in commands: | ||
| pod.execute(command=shlex.split(command), container=NET_TOOLS_CONTAINER_NAME) | ||
|
|
||
| _configure_external_frr_l3_vrf(pod=pod, vni=vni) | ||
|
|
||
| bare_ips = [ip.split("/")[0] for ip in endpoint_ips] | ||
| LOGGER.info(f"EVPN L3 endpoint deployed: {bare_ips} in namespace {_L3_ENDPOINT_NETNS}") | ||
|
|
||
| return EvpnEndpoint(pod=pod, ip_addresses=bare_ips, netns_name=_L3_ENDPOINT_NETNS) | ||
|
|
||
|
|
||
| def teardown_evpn_l3_endpoint(pod: Pod) -> None: | ||
| """Removes the EVPN L3 endpoint and VRF from the external FRR pod.""" | ||
| for cmd in [ | ||
| f"ip netns delete {_L3_ENDPOINT_NETNS}", | ||
| f"ip link delete {_L3_VETH_POD_SIDE}", | ||
| f"ip link delete {_L3_SVI_NAME}", | ||
| f"ip link delete {_L3_VRF_NAME}", | ||
| ]: | ||
| pod.execute(command=shlex.split(cmd), container=NET_TOOLS_CONTAINER_NAME, ignore_rc=True) | ||
|
|
||
| pod.execute( | ||
| command=["vtysh", "-c", "configure terminal", "-c", f"no router bgp {EXTERNAL_FRR_ASN} vrf {_L3_VRF_NAME}"], | ||
| ignore_rc=True, | ||
| ) | ||
|
|
||
| LOGGER.info(f"EVPN L3 endpoint removed: namespace={_L3_ENDPOINT_NETNS}, VRF={_L3_VRF_NAME}") | ||
|
|
||
|
|
||
| def _build_l3_endpoint_commands( | ||
| vni: int, | ||
| endpoint_ips: list[str], | ||
| gateway_ips: list[str], | ||
| ) -> list[str]: | ||
| return [ | ||
| "sysctl -w net.ipv4.ip_forward=1", | ||
| "sysctl -w net.ipv6.conf.all.forwarding=1", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may fail on single stack IPv4 setups (e.g. PSI). But I am not 100% sure,.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. single-stack means CNI configuration, not kernel limitation, right? the current setup is Linux kernel-level, the change is harmless on single-stack clusters per my understanding. But if we have kernel-level limitations on single-stack clusters, so... BTW, bgp/evpn tests work only on BM environments.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I asked Claude to explore our deployment scripts, short recap: "Findings: IPv6 is not disabled at boot time for IPv4 single-stack clusters. IPv6 remains enabled at the kernel level on nodes even in IPv4 single-stack clusters." Anyway, in a follow-up I will adjust the code to avoid future possible discrepancies. |
||
| f"ip link add {_L3_VRF_NAME} type vrf table {vni}", | ||
| f"ip link set {_L3_VRF_NAME} up", | ||
| f"bridge vlan add dev {_BRIDGE_NAME} vid {_L3_VID} self", | ||
| f"bridge vlan add dev {_VXLAN_NAME} vid {_L3_VID}", | ||
| f"bridge vni add dev {_VXLAN_NAME} vni {vni}", | ||
| f"bridge vlan add dev {_VXLAN_NAME} vid {_L3_VID} tunnel_info id {vni}", | ||
| f"ip link add {_L3_SVI_NAME} link {_BRIDGE_NAME} type vlan id {_L3_VID}", | ||
| f"ip link set {_L3_SVI_NAME} master {_L3_VRF_NAME}", | ||
| f"ip link set {_L3_SVI_NAME} up", | ||
| f"ip link add {_L3_VETH_POD_SIDE} type veth peer name {_L3_VETH_EP_SIDE}", | ||
| f"ip link set {_L3_VETH_POD_SIDE} master {_L3_VRF_NAME}", | ||
| *(f"ip addr add {ip} dev {_L3_VETH_POD_SIDE}" for ip in gateway_ips), | ||
| f"ip link set {_L3_VETH_POD_SIDE} up", | ||
| f"ip netns add {_L3_ENDPOINT_NETNS}", | ||
| f"ip link set {_L3_VETH_EP_SIDE} netns {_L3_ENDPOINT_NETNS}", | ||
| *(f"ip netns exec {_L3_ENDPOINT_NETNS} ip addr add {ip} dev {_L3_VETH_EP_SIDE}" for ip in endpoint_ips), | ||
| f"ip netns exec {_L3_ENDPOINT_NETNS} ip link set {_L3_VETH_EP_SIDE} up", | ||
| f"ip netns exec {_L3_ENDPOINT_NETNS} ip link set lo up", | ||
| *( | ||
| f"ip netns exec {_L3_ENDPOINT_NETNS} ip {'-6' if ':' in ip else ''} route add default" | ||
| f" via {ip.split('/')[0]}" | ||
| for ip in gateway_ips | ||
| ), | ||
| ] | ||
|
|
||
|
|
||
| def _configure_external_frr_l3_vrf(pod: Pod, vni: int) -> None: | ||
| config = "\n".join([ | ||
| f"vrf {_L3_VRF_NAME}", | ||
| f" vni {vni}", | ||
| "exit-vrf", | ||
| f"router bgp {EXTERNAL_FRR_ASN} vrf {_L3_VRF_NAME}", | ||
| " address-family ipv4 unicast", | ||
| " redistribute connected", | ||
| " exit-address-family", | ||
| " address-family ipv6 unicast", | ||
| " redistribute connected", | ||
| " exit-address-family", | ||
| " address-family l2vpn evpn", | ||
| f" rd {EXTERNAL_FRR_ASN}:{vni}", | ||
| f" route-target import {CLUSTER_FRR_ASN}:{vni}", | ||
| f" route-target export {CLUSTER_FRR_ASN}:{vni}", | ||
| " advertise ipv4 unicast", | ||
| " advertise ipv6 unicast", | ||
| " exit-address-family", | ||
| ]) | ||
| pod.execute(command=["vtysh", "-c", "configure terminal", "-c", config]) | ||
| _wait_for_l3_vrf_routes(pod=pod) | ||
|
|
||
| LOGGER.info(f"External FRR L3 VRF configured: {_L3_VRF_NAME} VNI {vni}") | ||
|
|
||
|
|
||
| @retry(wait_timeout=60, sleep=5, exceptions_dict={RuntimeError: []}) | ||
| def _wait_for_l3_vrf_routes(pod: Pod) -> bool: | ||
| output = pod.execute( | ||
| command=shlex.split(f"ip route show vrf {_L3_VRF_NAME} proto bgp"), | ||
| container=NET_TOOLS_CONTAINER_NAME, | ||
| ) | ||
| if not output.strip(): | ||
| raise RuntimeError(f"VRF {_L3_VRF_NAME} has no BGP routes") | ||
| return True | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def evpn_workloads_active_connections( | ||
| endpoint: EvpnEndpoint, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.