diff --git a/tests/network/l2_bridge/libl2bridge.py b/tests/network/l2_bridge/libl2bridge.py index 458c7ceef9..8dfe35b9af 100644 --- a/tests/network/l2_bridge/libl2bridge.py +++ b/tests/network/l2_bridge/libl2bridge.py @@ -12,7 +12,7 @@ from libs.net.ip import random_ipv4_address from libs.net.vmspec import lookup_iface_status, lookup_iface_status_ip, wait_for_missing_iface_status from libs.vm.factory import base_vmspec, fedora_vm -from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Multus, Network +from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Metadata, Multus, Network from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage from tests.network.libs import cloudinit from tests.network.libs.cloudinit import primary_iface_cloud_init @@ -380,6 +380,7 @@ def secondary_network_vm( secondary_iface_name: str, secondary_iface_addresses: list[str], affinity: Affinity | None = None, + labels: dict[str, str] | None = None, ) -> BaseVirtualMachine: """Create a Fedora VM with a masquerade primary interface and a secondary Linux bridge interface. @@ -391,6 +392,7 @@ def secondary_network_vm( secondary_iface_name: Name of the secondary network interface in the VM spec. secondary_iface_addresses: CIDR addresses to assign to the secondary interface via cloud-init. affinity: Optional node or pod affinity rules for scheduling. + labels: Optional labels to apply to the VM template metadata for pod scheduling. """ spec = base_vmspec() spec.template.spec.domain.devices.interfaces = [ # type: ignore @@ -404,6 +406,11 @@ def secondary_network_vm( if affinity: spec.template.spec.affinity = affinity + if labels: + spec.template.metadata = spec.template.metadata or Metadata() + spec.template.metadata.labels = spec.template.metadata.labels or {} + spec.template.metadata.labels.update(labels) + ethernets = {} primary = primary_iface_cloud_init() if primary: diff --git a/tests/network/l2_bridge/migration_stuntime/conftest.py b/tests/network/l2_bridge/migration_stuntime/conftest.py new file mode 100644 index 0000000000..dbcabd4f8c --- /dev/null +++ b/tests/network/l2_bridge/migration_stuntime/conftest.py @@ -0,0 +1,110 @@ +from collections.abc import Generator +from typing import Final + +import pytest +from kubernetes.dynamic import DynamicClient +from ocp_resources.namespace import Namespace + +import tests.network.libs.nodenetworkconfigurationpolicy as libnncp +from libs.net import netattachdef as libnad +from libs.net.ip import random_ipv4_address, random_ipv6_address +from libs.net.vmspec import lookup_iface_status_ip +from libs.vm.affinity import new_pod_affinity +from libs.vm.vm import BaseVirtualMachine +from tests.network.l2_bridge.libl2bridge import secondary_network_vm +from tests.network.libs.stuntime import SERVER_VM_LABEL, ContinuousPing + +STUNTIME_BRIDGE_IFACE_NAME: Final[str] = "stuntime-bridge" + + +@pytest.fixture(scope="module") +def l2_bridge_stuntime_nad( + admin_client: DynamicClient, + namespace: Namespace, + bridge_nncp: libnncp.NodeNetworkConfigurationPolicy, +) -> Generator[libnad.NetworkAttachmentDefinition]: + nad_name = "l2-bridge-nad" + config = libnad.NetConfig( + name=nad_name, + plugins=[libnad.CNIPluginBridgeConfig(bridge=bridge_nncp.desired_state_spec.interfaces[0].name)], # type: ignore[index] + ) + with libnad.NetworkAttachmentDefinition( + name=nad_name, + namespace=namespace.name, + config=config, + client=admin_client, + ) as nad: + yield nad + + +@pytest.fixture(scope="class") +def stuntime_server_vm( + unprivileged_client: DynamicClient, + namespace: Namespace, + l2_bridge_ip_family: int, + l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition, +) -> Generator[BaseVirtualMachine]: + with secondary_network_vm( + namespace=namespace.name, + name="l2bridge-stuntime-server", + client=unprivileged_client, + nad_name=l2_bridge_stuntime_nad.name, + secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME, + secondary_iface_addresses=[ + f"{random_ipv4_address(net_seed=0, host_address=1)}/24", + f"{random_ipv6_address(net_seed=0, host_address=1)}/64", + ], + labels=dict([SERVER_VM_LABEL]), + ) as server_vm: + server_vm.start(wait=True) + server_vm.wait_for_agent_connected() + yield server_vm + + +@pytest.fixture(scope="class") +def stuntime_client_vm( + unprivileged_client: DynamicClient, + namespace: Namespace, + l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition, + stuntime_server_vm: BaseVirtualMachine, +) -> Generator[BaseVirtualMachine]: + with secondary_network_vm( + namespace=namespace.name, + name="l2bridge-stuntime-client", + client=unprivileged_client, + nad_name=l2_bridge_stuntime_nad.name, + secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME, + secondary_iface_addresses=[ + f"{random_ipv4_address(net_seed=0, host_address=2)}/24", + f"{random_ipv6_address(net_seed=0, host_address=2)}/64", + ], + affinity=new_pod_affinity(label=SERVER_VM_LABEL), + ) as client_vm: + client_vm.start(wait=True) + client_vm.wait_for_agent_connected() + yield client_vm + + +@pytest.fixture(scope="class") +def l2_bridge_ip_family(request: pytest.FixtureRequest) -> int: + """IP family for stuntime measurement, activated by per-test parametrize.""" + return request.param + + +@pytest.fixture() +def l2_bridge_active_ping( + l2_bridge_ip_family: int, + stuntime_server_vm: BaseVirtualMachine, + stuntime_client_vm: BaseVirtualMachine, +) -> Generator[ContinuousPing]: + """Continuous ping session from client to server for stuntime measurement.""" + server_ip = str( + lookup_iface_status_ip( + vm=stuntime_server_vm, + iface_name=STUNTIME_BRIDGE_IFACE_NAME, + ip_family=l2_bridge_ip_family, + ) + ) + + with ContinuousPing(source_vm=stuntime_client_vm, destination_ip=server_ip) as ping: + yield ping diff --git a/tests/network/l2_bridge/migration_stuntime/test_migration_stuntime.py b/tests/network/l2_bridge/migration_stuntime/test_migration_stuntime.py index e6f9ff6ed6..038b19611e 100644 --- a/tests/network/l2_bridge/migration_stuntime/test_migration_stuntime.py +++ b/tests/network/l2_bridge/migration_stuntime/test_migration_stuntime.py @@ -6,25 +6,27 @@ Stuntime is defined as the connectivity gap from last successful reply before loss to first successful reply after recovery. -Stuntime is measured using ICMP ping from client to server in 0.1s intervals, using ping -D so each -log line includes a timestamp for gap calculation. -The under-test VMs are configured on a Linux bridge secondary network, with a single interface, +Stuntime is measured using ICMP ping from client to server in 0.1s intervals. +The under-test VMs are configured with a secondary Linux bridge interface, on which IPv4/IPv6 static addresses will be defined according to the environment the test runs on. Client - The connectivity initiator VM that runs continuous ping toward the server VM. Server - The connectivity listener VM that receives the ping and responds. -STP Reference: -https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md +STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md """ import pytest -__test__ = False +from libs.vm.affinity import new_pod_anti_affinity +from tests.network.libs.stuntime import SERVER_VM_LABEL, STUNTIME_THRESHOLD_SECONDS, measure_stuntime +from utilities.virt import migrate_vm_and_verify + +pytestmark = [pytest.mark.tier3] """ Parametrize: - - ip_family: + - l2_bridge_ip_family: - ipv4 [Markers: ipv4] - ipv6 [Markers: ipv6] @@ -36,9 +38,19 @@ @pytest.mark.incremental +@pytest.mark.parametrize( + "l2_bridge_ip_family", + [ + pytest.param(4, marks=pytest.mark.ipv4, id="ipv4"), + pytest.param(6, marks=pytest.mark.ipv6, id="ipv6"), + ], + indirect=True, +) class TestMigrationStuntime: @pytest.mark.polarion("CNV-15252") - def test_client_migrates_off_server_node(self): + def test_client_migrates_off_server_node( + self, admin_client, l2_bridge_ip_family, stuntime_client_vm, l2_bridge_active_ping + ): """ Test that measured stuntime does not exceed the global threshold when the client VM migrates from the node hosting the server VM into a different node. @@ -58,6 +70,12 @@ def test_client_migrates_off_server_node(self): Expected: - Measured stuntime does not exceed the global threshold. """ + stuntime_client_vm.set_template_affinity(affinity=new_pod_anti_affinity(label=SERVER_VM_LABEL)) + migrate_vm_and_verify(vm=stuntime_client_vm, client=admin_client) + measured_stuntime = measure_stuntime(active_ping=l2_bridge_active_ping) + assert measured_stuntime <= STUNTIME_THRESHOLD_SECONDS, ( + f"Stuntime {measured_stuntime}s exceeds threshold ({STUNTIME_THRESHOLD_SECONDS}s)" + ) @pytest.mark.polarion("CNV-15253") def test_client_migrates_between_non_server_nodes(self): @@ -168,3 +186,9 @@ def test_server_migrates_to_client_node(self): Expected: - Measured stuntime does not exceed the global threshold. """ + + test_client_migrates_between_non_server_nodes.__test__ = False + test_client_migrates_to_server_node.__test__ = False + test_server_migrates_off_client_node.__test__ = False + test_server_migrates_between_non_client_nodes.__test__ = False + test_server_migrates_to_client_node.__test__ = False diff --git a/tests/network/localnet/migration_stuntime/libstuntime.py b/tests/network/libs/stuntime.py similarity index 93% rename from tests/network/localnet/migration_stuntime/libstuntime.py rename to tests/network/libs/stuntime.py index b48565e6f4..ff2d1bf6cb 100644 --- a/tests/network/localnet/migration_stuntime/libstuntime.py +++ b/tests/network/libs/stuntime.py @@ -1,4 +1,4 @@ -"""Helpers for OVN localnet migration stuntime tests.""" +"""Helpers for migration stuntime tests.""" import ipaddress import logging @@ -23,35 +23,6 @@ class InsufficientStuntimeDataError(ValueError): """Raised when ping log has too few successful replies to compute stuntime.""" -def measure_stuntime(active_ping: "ContinuousPing") -> float: - """Stop the continuous ping session and compute the measured stuntime. - - Args: - active_ping: Active continuous ping session to stop and evaluate. - - Returns: - Measured stuntime in seconds. - """ - active_ping.stop() - _, _, lost = active_ping.report() - return compute_stuntime(lost_packets=lost) - - -def compute_stuntime(lost_packets: int) -> float: - """Compute stuntime from lost packet count. - - Args: - lost_packets: Number of packets lost during migration. - - Returns: - Stuntime in seconds (connectivity gap). - """ - # Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery - stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS - LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)") - return stuntime - - class ContinuousPing: """Context manager for continuous ping monitoring during VM operations. @@ -141,3 +112,32 @@ def _verify_ping_reaches_destination(self) -> None: ], timeout=DEFAULT_COMMAND_TIMEOUT_SECONDS + 5, ) + + +def measure_stuntime(active_ping: ContinuousPing) -> float: + """Stop a continuous ping session and compute the stuntime. + + Args: + active_ping: Active ContinuousPing session to stop and evaluate. + + Returns: + Measured stuntime in seconds. + """ + active_ping.stop() + _, _, lost = active_ping.report() + return _compute_stuntime(lost_packets=lost) + + +def _compute_stuntime(lost_packets: int) -> float: + """Compute stuntime from lost packet count. + + Args: + lost_packets: Number of packets lost during migration. + + Returns: + Stuntime in seconds (connectivity gap). + """ + # Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery + stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS + LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)") + return stuntime diff --git a/tests/network/localnet/migration_stuntime/conftest.py b/tests/network/localnet/migration_stuntime/conftest.py index e08c90300d..08ba245d96 100644 --- a/tests/network/localnet/migration_stuntime/conftest.py +++ b/tests/network/localnet/migration_stuntime/conftest.py @@ -11,6 +11,7 @@ from libs.vm.vm import BaseVirtualMachine from tests.network.libs import cloudinit from tests.network.libs import cluster_user_defined_network as libcudn +from tests.network.libs.stuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing from tests.network.localnet.liblocalnet import ( GUEST_1ST_IFACE_NAME, LOCALNET_OVS_BRIDGE_INTERFACE, @@ -18,7 +19,6 @@ libnncp, localnet_vm, ) -from tests.network.localnet.migration_stuntime.libstuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing LOGGER = logging.getLogger(__name__) diff --git a/tests/network/localnet/migration_stuntime/test_migration_stuntime.py b/tests/network/localnet/migration_stuntime/test_migration_stuntime.py index 625afe5197..a191782120 100644 --- a/tests/network/localnet/migration_stuntime/test_migration_stuntime.py +++ b/tests/network/localnet/migration_stuntime/test_migration_stuntime.py @@ -13,14 +13,13 @@ Client - The connectivity initiator VM that runs continuous ping toward the server VM. Server - The connectivity listener VM that receives the ping and responds. -STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main -/stps/sig-network/stuntime_measurement.md +STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md """ import pytest from libs.vm.affinity import new_pod_affinity, new_pod_anti_affinity -from tests.network.localnet.migration_stuntime.libstuntime import ( +from tests.network.libs.stuntime import ( CLIENT_VM_LABEL, SERVER_VM_LABEL, STUNTIME_THRESHOLD_SECONDS,