diff --git a/libs/net/traffic_generator.py b/libs/net/traffic_generator.py index f0e5969cdd..14b0492f87 100644 --- a/libs/net/traffic_generator.py +++ b/libs/net/traffic_generator.py @@ -1,15 +1,18 @@ +import contextlib import logging from abc import ABC, abstractmethod -from typing import Final +from typing import Final, Generator from ocp_resources.pod import Pod from ocp_utilities.exceptions import CommandExecFailed from timeout_sampler import retry +from libs.net.vmspec import IP_ADDRESS, lookup_iface_status from libs.vm.vm import BaseVirtualMachine _DEFAULT_CMD_TIMEOUT_SEC: Final[int] = 10 _IPERF_BIN: Final[str] = "iperf3" +IPERF_SERVER_PORT: Final[int] = 5201 LOGGER = logging.getLogger(__name__) @@ -186,3 +189,41 @@ def _ensure_is_running(self) -> bool: def is_tcp_connection(server: TcpServer, client: BaseTcpClient) -> bool: return server.is_running() and client.is_running() + + +@contextlib.contextmanager +def client_server_active_connection( + client_vm: BaseVirtualMachine, + server_vm: BaseVirtualMachine, + spec_logical_network: str, + port: int = IPERF_SERVER_PORT, + maximum_segment_size: int = 0, +) -> Generator[tuple[VMTcpClient, TcpServer], None, None]: + """Start iperf3 client-server connection with continuous TCP traffic flow. + + Automatically starts an iperf3 server and client, with traffic flowing continuously + while inside the context. Both processes stop automatically on exit. + + Args: + client_vm: VM running the iperf3 client (sends traffic). + server_vm: VM running the iperf3 server (receives traffic). + spec_logical_network: Network interface name on server VM for IP resolution. + port: TCP port for iperf3 connection. + maximum_segment_size: Define explicitly the TCP payload size (in bytes). + Use for jumbo frame testing. + Default value is 0 (do not change mss). + + Yields: + tuple[VMTcpClient, TcpServer]: Client and server objects with active traffic flowing. + + Note: + Traffic runs with infinite duration until context exits. + """ + with TcpServer(vm=server_vm, port=port) as server: + with VMTcpClient( + vm=client_vm, + server_ip=lookup_iface_status(vm=server_vm, iface_name=spec_logical_network)[IP_ADDRESS], + server_port=port, + maximum_segment_size=maximum_segment_size, + ) as client: + yield client, server diff --git a/libs/net/udn.py b/libs/net/udn.py index 4a01c7dea6..36eaa35608 100644 --- a/libs/net/udn.py +++ b/libs/net/udn.py @@ -7,11 +7,12 @@ from libs.vm.spec import Interface, NetBinding, Network from utilities.infra import create_ns -UDN_BINDING_PLUGIN_NAME: Final[str] = "l2bridge" +UDN_BINDING_DEFAULT_PLUGIN_NAME: Final[str] = "l2bridge" +UDN_BINDING_PASST_PLUGIN_NAME: Final[str] = "passt" -def udn_primary_network(name: str) -> tuple[Interface, Network]: - return Interface(name=name, binding=NetBinding(name=UDN_BINDING_PLUGIN_NAME)), Network(name=name, pod={}) +def udn_primary_network(name: str, binding: str) -> tuple[Interface, Network]: + return Interface(name=name, binding=NetBinding(name=binding)), Network(name=name, pod={}) def create_udn_namespace( diff --git a/tests/network/bgp/conftest.py b/tests/network/bgp/conftest.py index ee28ce013e..8d812ef8a7 100644 --- a/tests/network/bgp/conftest.py +++ b/tests/network/bgp/conftest.py @@ -14,7 +14,7 @@ from libs.net import netattachdef as libnad from libs.net.traffic_generator import PodTcpClient as TcpClient from libs.net.traffic_generator import TcpServer -from libs.net.udn import create_udn_namespace +from libs.net.udn import UDN_BINDING_DEFAULT_PLUGIN_NAME, create_udn_namespace from libs.net.vmspec import IP_ADDRESS, lookup_iface_status, lookup_primary_network from libs.vm.vm import BaseVirtualMachine from tests.network.libs import cluster_user_defined_network as libcudn @@ -210,6 +210,7 @@ def vm_cudn( namespace_name=namespace_cudn.name, name="vm-cudn-bgp", client=admin_client, + binding=UDN_BINDING_DEFAULT_PLUGIN_NAME, template_labels=EXTERNAL_FRR_POD_LABEL, anti_affinity_namespaces=[frr_external_pod.pod.namespace], ) as vm: diff --git a/tests/network/libs/vm_factory.py b/tests/network/libs/vm_factory.py index 1ee134879d..b4e12b23d7 100644 --- a/tests/network/libs/vm_factory.py +++ b/tests/network/libs/vm_factory.py @@ -12,11 +12,12 @@ def udn_vm( namespace_name: str, name: str, client: DynamicClient, + binding: str, template_labels: dict | None = None, anti_affinity_namespaces: list[str] | None = None, ) -> BaseVirtualMachine: spec = base_vmspec() - iface, network = udn_primary_network(name="udn-primary") + iface, network = udn_primary_network(name="udn-primary", binding=binding) spec.template.spec.domain.devices.interfaces = [iface] # type: ignore spec.template.spec.networks = [network] if template_labels: diff --git a/tests/network/localnet/conftest.py b/tests/network/localnet/conftest.py index 8237164991..834b1e80ea 100644 --- a/tests/network/localnet/conftest.py +++ b/tests/network/localnet/conftest.py @@ -5,7 +5,7 @@ from ocp_resources.namespace import Namespace import tests.network.libs.nodenetworkconfigurationpolicy as libnncp -from libs.net.traffic_generator import TcpServer +from libs.net.traffic_generator import TcpServer, client_server_active_connection from libs.net.traffic_generator import VMTcpClient as TcpClient from libs.net.vmspec import lookup_iface_status from libs.vm.spec import Interface, Multus, Network @@ -14,7 +14,6 @@ from tests.network.libs import cluster_user_defined_network as libcudn from tests.network.libs.ip import IPV4_HEADER_SIZE, TCP_HEADER_SIZE, random_ipv4_address from tests.network.localnet.liblocalnet import ( - _IPERF_SERVER_PORT, LINK_STATE_DOWN, LOCALNET_BR_EX_INTERFACE, LOCALNET_BR_EX_INTERFACE_NO_VLAN, @@ -23,7 +22,6 @@ LOCALNET_OVS_BRIDGE_INTERFACE, LOCALNET_OVS_BRIDGE_NETWORK, LOCALNET_TEST_LABEL, - client_server_active_connection, create_nncp_localnet_on_secondary_node_nic, create_traffic_client, create_traffic_server, @@ -469,7 +467,6 @@ def localnet_ovs_bridge_jumbo_frame_client_and_server_vms( client_vm=ovs_bridge_localnet_running_jumbo_frame_vms[1], server_vm=ovs_bridge_localnet_running_jumbo_frame_vms[0], spec_logical_network=LOCALNET_OVS_BRIDGE_INTERFACE, - port=_IPERF_SERVER_PORT, maximum_segment_size=cluster_hardware_mtu - IPV4_HEADER_SIZE - TCP_HEADER_SIZE, ) as (client, server): yield client, server diff --git a/tests/network/localnet/liblocalnet.py b/tests/network/localnet/liblocalnet.py index 260c1b62b5..80bc3b34aa 100644 --- a/tests/network/localnet/liblocalnet.py +++ b/tests/network/localnet/liblocalnet.py @@ -6,7 +6,7 @@ from kubernetes.client import ApiException from kubernetes.dynamic import DynamicClient -from libs.net.traffic_generator import TcpServer +from libs.net.traffic_generator import IPERF_SERVER_PORT, TcpServer from libs.net.traffic_generator import VMTcpClient as TcpClient from libs.net.vmspec import IP_ADDRESS, add_volume_disk, lookup_iface_status from libs.vm.affinity import new_pod_anti_affinity @@ -29,7 +29,6 @@ LINK_STATE_UP = "up" LINK_STATE_DOWN = "down" NNCP_INTERFACE_TYPE_ETHERNET = "ethernet" -_IPERF_SERVER_PORT = 5201 LOGGER = logging.getLogger(__name__) @@ -48,7 +47,7 @@ def run_vms(vms: tuple[BaseVirtualMachine, ...]) -> tuple[BaseVirtualMachine, .. def create_traffic_server(vm: BaseVirtualMachine) -> TcpServer: - return TcpServer(vm=vm, port=_IPERF_SERVER_PORT) + return TcpServer(vm=vm, port=IPERF_SERVER_PORT) def create_traffic_client( @@ -57,7 +56,7 @@ def create_traffic_client( return TcpClient( vm=client_vm, server_ip=lookup_iface_status(vm=server_vm, iface_name=spec_logical_network)[IP_ADDRESS], - server_port=_IPERF_SERVER_PORT, + server_port=IPERF_SERVER_PORT, ) @@ -183,44 +182,6 @@ def localnet_cudn( ) -@contextlib.contextmanager -def client_server_active_connection( - client_vm: BaseVirtualMachine, - server_vm: BaseVirtualMachine, - spec_logical_network: str, - port: int = _IPERF_SERVER_PORT, - maximum_segment_size: int = 0, -) -> Generator[tuple[TcpClient, TcpServer], None, None]: - """Start iperf3 client-server connection with continuous TCP traffic flow. - - Automatically starts an iperf3 server and client, with traffic flowing continuously - while inside the context. Both processes stop automatically on exit. - - Args: - client_vm: VM running the iperf3 client (sends traffic). - server_vm: VM running the iperf3 server (receives traffic). - spec_logical_network: Network interface name on server VM for IP resolution. - port: TCP port for iperf3 connection. - maximum_segment_size: Define explicitly the TCP payload size (in bytes). - Use for jumbo frame testing. - Default value is 0 (do not change mss). - - Yields: - tuple[TcpClient, TcpServer]: Client and server objects with active traffic flowing. - - Note: - Traffic runs with infinite duration until context exits. - """ - with TcpServer(vm=server_vm, port=port) as server: - with TcpClient( - vm=client_vm, - server_ip=lookup_iface_status(vm=server_vm, iface_name=spec_logical_network)[IP_ADDRESS], - server_port=port, - maximum_segment_size=maximum_segment_size, - ) as client: - yield client, server - - @contextlib.contextmanager def create_nncp_localnet_on_secondary_node_nic( node_nic_name: str, diff --git a/tests/network/localnet/test_default_bridge.py b/tests/network/localnet/test_default_bridge.py index ec33778dcc..9e9330db3f 100644 --- a/tests/network/localnet/test_default_bridge.py +++ b/tests/network/localnet/test_default_bridge.py @@ -2,12 +2,11 @@ import pytest -from libs.net.traffic_generator import is_tcp_connection +from libs.net.traffic_generator import client_server_active_connection, is_tcp_connection from libs.net.vmspec import IP_ADDRESS, lookup_iface_status from tests.network.localnet.liblocalnet import ( LOCALNET_BR_EX_INTERFACE, LOCALNET_BR_EX_INTERFACE_NO_VLAN, - client_server_active_connection, ) from utilities.virt import migrate_vm_and_verify diff --git a/tests/network/localnet/test_ovs_bridge.py b/tests/network/localnet/test_ovs_bridge.py index cc1a29ea8a..d593463a6b 100644 --- a/tests/network/localnet/test_ovs_bridge.py +++ b/tests/network/localnet/test_ovs_bridge.py @@ -1,11 +1,10 @@ import pytest -from libs.net.traffic_generator import is_tcp_connection +from libs.net.traffic_generator import client_server_active_connection, is_tcp_connection from libs.net.vmspec import lookup_iface_status from tests.network.localnet.liblocalnet import ( LINK_STATE_UP, LOCALNET_OVS_BRIDGE_INTERFACE, - client_server_active_connection, ) from utilities.constants import QUARANTINED from utilities.virt import migrate_vm_and_verify diff --git a/tests/network/user_defined_network/conftest.py b/tests/network/user_defined_network/conftest.py new file mode 100644 index 0000000000..311f88eb6f --- /dev/null +++ b/tests/network/user_defined_network/conftest.py @@ -0,0 +1,37 @@ +import pytest +from ocp_resources.user_defined_network import Layer2UserDefinedNetwork + +from libs.vm import affinity +from tests.network.libs.ip import random_ipv4_address +from utilities.infra import create_ns + + +@pytest.fixture(scope="module") +def udn_namespace(admin_client): + yield from create_ns( + admin_client=admin_client, + name="test-user-defined-network-ns", + labels={"k8s.ovn.org/primary-user-defined-network": ""}, + ) + + +@pytest.fixture(scope="module") +def namespaced_layer2_user_defined_network(admin_client, udn_namespace): + with Layer2UserDefinedNetwork( + name="layer2-udn", + namespace=udn_namespace.name, + role="Primary", + subnets=[f"{random_ipv4_address(net_seed=0, host_address=0)}/24"], + ipam={"lifecycle": "Persistent"}, + client=admin_client, + ) as udn: + udn.wait_for_condition( + condition="NetworkAllocationSucceeded", + status=udn.Condition.Status.TRUE, + ) + yield udn + + +@pytest.fixture(scope="module") +def udn_affinity_label(): + return affinity.new_label(key_prefix="udn") diff --git a/tests/network/user_defined_network/test_user_defined_network.py b/tests/network/user_defined_network/test_user_defined_network.py index 7a4767bb76..659daccc6a 100644 --- a/tests/network/user_defined_network/test_user_defined_network.py +++ b/tests/network/user_defined_network/test_user_defined_network.py @@ -1,60 +1,27 @@ import ipaddress import pytest -from ocp_resources.user_defined_network import Layer2UserDefinedNetwork from ocp_resources.utils.constants import TIMEOUT_1MINUTE from libs.net.traffic_generator import TcpServer, is_tcp_connection from libs.net.traffic_generator import VMTcpClient as TcpClient +from libs.net.udn import UDN_BINDING_DEFAULT_PLUGIN_NAME from libs.net.vmspec import lookup_iface_status, lookup_primary_network -from libs.vm import affinity -from tests.network.libs.ip import random_ipv4_address from tests.network.libs.vm_factory import udn_vm from utilities.constants import PUBLIC_DNS_SERVER_IP, TIMEOUT_1MIN -from utilities.infra import create_ns from utilities.virt import migrate_vm_and_verify IP_ADDRESS = "ipAddress" SERVER_PORT = 5201 -@pytest.fixture(scope="module") -def udn_namespace(admin_client): - yield from create_ns( - admin_client=admin_client, - name="test-user-defined-network-ns", - labels={"k8s.ovn.org/primary-user-defined-network": ""}, - ) - - -@pytest.fixture(scope="module") -def namespaced_layer2_user_defined_network(admin_client, udn_namespace): - with Layer2UserDefinedNetwork( - name="layer2-udn", - namespace=udn_namespace.name, - role="Primary", - subnets=[f"{random_ipv4_address(net_seed=0, host_address=0)}/24"], - ipam={"lifecycle": "Persistent"}, - client=admin_client, - ) as udn: - udn.wait_for_condition( - condition="NetworkAllocationSucceeded", - status=udn.Condition.Status.TRUE, - ) - yield udn - - -@pytest.fixture(scope="class") -def udn_affinity_label(): - return affinity.new_label(key_prefix="udn") - - @pytest.fixture(scope="class") def vma_udn(udn_namespace, namespaced_layer2_user_defined_network, udn_affinity_label, admin_client): with udn_vm( namespace_name=udn_namespace.name, name="vma-udn", client=admin_client, + binding=UDN_BINDING_DEFAULT_PLUGIN_NAME, template_labels=dict((udn_affinity_label,)), ) as vm: vm.start(wait=True) @@ -68,6 +35,7 @@ def vmb_udn(udn_namespace, namespaced_layer2_user_defined_network, udn_affinity_ namespace_name=udn_namespace.name, name="vmb-udn", client=admin_client, + binding=UDN_BINDING_DEFAULT_PLUGIN_NAME, template_labels=dict((udn_affinity_label,)), ) as vm: vm.start(wait=True) diff --git a/tests/network/user_defined_network/test_user_defined_network_passt.py b/tests/network/user_defined_network/test_user_defined_network_passt.py new file mode 100644 index 0000000000..ee84de8518 --- /dev/null +++ b/tests/network/user_defined_network/test_user_defined_network_passt.py @@ -0,0 +1,105 @@ +from typing import Generator + +import pytest +from kubernetes.dynamic import DynamicClient +from ocp_resources.hyperconverged import HyperConverged +from ocp_resources.kubevirt import KubeVirt +from ocp_resources.namespace import Namespace +from ocp_resources.user_defined_network import Layer2UserDefinedNetwork +from timeout_sampler import TimeoutExpiredError, retry + +from libs.net.traffic_generator import client_server_active_connection, is_tcp_connection +from libs.net.udn import UDN_BINDING_PASST_PLUGIN_NAME +from libs.net.vmspec import lookup_primary_network +from libs.vm.vm import BaseVirtualMachine +from tests.network.libs.vm_factory import udn_vm +from utilities.hco import ResourceEditorValidateHCOReconcile +from utilities.virt import LOGGER, migrate_vm_and_verify + + +@retry(wait_timeout=400, sleep=10, exceptions_dict={}) +def wait_for_ready_vm_with_restart(vm: BaseVirtualMachine) -> bool: + try: + vm.wait_for_ready_status(status=True, timeout=90) + except TimeoutExpiredError: + LOGGER.warning(f"For {vm.name}: Waited for Ready condition but got timeout, restarting vm") + vm.restart() + return False + return True + + +@pytest.fixture(scope="module") +def passt_enabled_in_hco( + hyperconverged_resource_scope_module: HyperConverged, +) -> Generator[None, None, None]: + with ResourceEditorValidateHCOReconcile( + patches={ + hyperconverged_resource_scope_module: { + "metadata": {"annotations": {"hco.kubevirt.io/deployPasstNetworkBinding": "true"}} + } + }, + list_resource_reconcile=[KubeVirt], + wait_for_reconcile_post_update=True, + ): + yield + + +@pytest.fixture(scope="module") +def passt_running_vm_pair( + udn_namespace: Namespace, + namespaced_layer2_user_defined_network: Layer2UserDefinedNetwork, + udn_affinity_label: tuple[str, str], + admin_client: DynamicClient, + passt_enabled_in_hco, +) -> Generator[tuple[BaseVirtualMachine, BaseVirtualMachine], None, None]: + with ( + udn_vm( + namespace_name=udn_namespace.name, + name="vma-passt", + client=admin_client, + template_labels=dict((udn_affinity_label,)), + binding=UDN_BINDING_PASST_PLUGIN_NAME, + ) as vm_a, + udn_vm( + namespace_name=udn_namespace.name, + name="vmb-passt", + client=admin_client, + template_labels=dict((udn_affinity_label,)), + binding=UDN_BINDING_PASST_PLUGIN_NAME, + ) as vm_b, + ): + vm_a.start(wait=False) + vm_b.start(wait=False) + # passt may not yet be registered. Try to start the VM and if it does not run in time, + # retry by restarting the VM and waiting again + wait_for_ready_vm_with_restart(vm=vm_a) + wait_for_ready_vm_with_restart(vm=vm_b) + vm_a.wait_for_agent_connected() + vm_b.wait_for_agent_connected() + yield vm_a, vm_b + + +@pytest.mark.ipv4 +@pytest.mark.single_nic +@pytest.mark.polarion("CNV-12427") +def test_passt_connectivity_is_preserved_during_client_live_migration(passt_enabled_in_hco, passt_running_vm_pair): + with client_server_active_connection( + client_vm=passt_running_vm_pair[0], + server_vm=passt_running_vm_pair[1], + spec_logical_network=lookup_primary_network(vm=passt_running_vm_pair[1]).name, + ) as (client, server): + migrate_vm_and_verify(vm=client.vm) + assert is_tcp_connection(server=server, client=client) + + +@pytest.mark.ipv4 +@pytest.mark.single_nic +@pytest.mark.polarion("CNV-12428") +def test_passt_connectivity_is_preserved_during_server_live_migration(passt_enabled_in_hco, passt_running_vm_pair): + with client_server_active_connection( + client_vm=passt_running_vm_pair[0], + server_vm=passt_running_vm_pair[1], + spec_logical_network=lookup_primary_network(vm=passt_running_vm_pair[1]).name, + ) as (client, server): + migrate_vm_and_verify(vm=server.vm) + assert is_tcp_connection(server=server, client=client)