From b7e75dd75b14aebfa87619130ae36b23eb825471 Mon Sep 17 00:00:00 2001 From: Asia Khromov Date: Sun, 31 May 2026 12:39:39 +0300 Subject: [PATCH 1/7] net: Move apimachinery and nncp modules to libs These modules have no dependency on test-specific code. Moving them to libs/net/ makes them importable from any test suite without cross-package imports. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Asia Khromov --- {tests/network/libs => libs/net}/apimachinery.py | 0 .../network/libs => libs/net}/nodenetworkconfigurationpolicy.py | 2 +- tests/network/bgp/conftest.py | 2 +- tests/network/l2_bridge/bandwidth/conftest.py | 2 +- tests/network/l2_bridge/conftest.py | 2 +- tests/network/l2_bridge/migration_stuntime/conftest.py | 2 +- tests/network/l2_bridge/nad_ref_change/conftest.py | 2 +- tests/network/libs/cloudinit.py | 2 +- tests/network/libs/cluster_user_defined_network.py | 2 +- tests/network/localnet/conftest.py | 2 +- tests/network/localnet/ipam/conftest.py | 2 +- tests/network/localnet/liblocalnet.py | 2 +- 12 files changed, 11 insertions(+), 11 deletions(-) rename {tests/network/libs => libs/net}/apimachinery.py (100%) rename {tests/network/libs => libs/net}/nodenetworkconfigurationpolicy.py (98%) diff --git a/tests/network/libs/apimachinery.py b/libs/net/apimachinery.py similarity index 100% rename from tests/network/libs/apimachinery.py rename to libs/net/apimachinery.py diff --git a/tests/network/libs/nodenetworkconfigurationpolicy.py b/libs/net/nodenetworkconfigurationpolicy.py similarity index 98% rename from tests/network/libs/nodenetworkconfigurationpolicy.py rename to libs/net/nodenetworkconfigurationpolicy.py index 0269bbb685..2476da7945 100644 --- a/tests/network/libs/nodenetworkconfigurationpolicy.py +++ b/libs/net/nodenetworkconfigurationpolicy.py @@ -12,7 +12,7 @@ from ocp_resources.resource import Resource, ResourceEditor from timeout_sampler import retry -from tests.network.libs.apimachinery import dict_normalization_for_dataclass +from libs.net.apimachinery import dict_normalization_for_dataclass WAIT_FOR_STATUS_TIMEOUT_SEC = 120 WAIT_FOR_STATUS_INTERVAL_SEC = 5 diff --git a/tests/network/bgp/conftest.py b/tests/network/bgp/conftest.py index 7059f0284b..40d520df33 100644 --- a/tests/network/bgp/conftest.py +++ b/tests/network/bgp/conftest.py @@ -12,6 +12,7 @@ from ocp_resources.node import Node from libs.net import netattachdef as libnad +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import random_ipv4_address from libs.net.traffic_generator import PodTcpClient as TcpClient from libs.net.traffic_generator import TcpServer @@ -19,7 +20,6 @@ from libs.net.vmspec import lookup_iface_status_ip, lookup_primary_network from libs.vm.vm import BaseVirtualMachine from tests.network.libs import cluster_user_defined_network as libcudn -from tests.network.libs import nodenetworkconfigurationpolicy as libnncp from tests.network.libs.bgp import ( EXTERNAL_FRR_POD_LABEL, NET_TOOLS_CONTAINER_NAME, diff --git a/tests/network/l2_bridge/bandwidth/conftest.py b/tests/network/l2_bridge/bandwidth/conftest.py index b81c8ede1c..7903469735 100644 --- a/tests/network/l2_bridge/bandwidth/conftest.py +++ b/tests/network/l2_bridge/bandwidth/conftest.py @@ -7,7 +7,7 @@ from kubernetes.dynamic import DynamicClient from ocp_resources.namespace import Namespace -import tests.network.libs.nodenetworkconfigurationpolicy as libnncp +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import random_ip_addresses_by_family from libs.net.netattachdef import ( CNIPluginBandwidthConfig, diff --git a/tests/network/l2_bridge/conftest.py b/tests/network/l2_bridge/conftest.py index e369c025f4..7509ee428b 100644 --- a/tests/network/l2_bridge/conftest.py +++ b/tests/network/l2_bridge/conftest.py @@ -5,7 +5,7 @@ from kubernetes.dynamic import DynamicClient from pyhelper_utils.shell import run_ssh_commands -import tests.network.libs.nodenetworkconfigurationpolicy as libnncp +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import random_ipv4_address from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition from tests.network.l2_bridge.libl2bridge import DHCP_INTERFACE_NAME, bridge_attached_vm diff --git a/tests/network/l2_bridge/migration_stuntime/conftest.py b/tests/network/l2_bridge/migration_stuntime/conftest.py index dbcabd4f8c..d41627a48e 100644 --- a/tests/network/l2_bridge/migration_stuntime/conftest.py +++ b/tests/network/l2_bridge/migration_stuntime/conftest.py @@ -5,8 +5,8 @@ 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 import nodenetworkconfigurationpolicy as libnncp 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 diff --git a/tests/network/l2_bridge/nad_ref_change/conftest.py b/tests/network/l2_bridge/nad_ref_change/conftest.py index 9eea5af6ff..c226d46536 100644 --- a/tests/network/l2_bridge/nad_ref_change/conftest.py +++ b/tests/network/l2_bridge/nad_ref_change/conftest.py @@ -4,6 +4,7 @@ from kubernetes.dynamic import DynamicClient from ocp_resources.namespace import Namespace +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import filter_link_local_addresses, random_cidr_addresses_by_family from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition from libs.net.vmspec import lookup_iface_status, wait_for_ifaces_status @@ -15,7 +16,6 @@ NET_SEED, two_secondary_bridge_vm, ) -from tests.network.libs import nodenetworkconfigurationpolicy as libnncp from tests.network.libs.connectivity import ARP_ISOLATION_SYSCTL_CMD, poll_tcp_connectivity diff --git a/tests/network/libs/cloudinit.py b/tests/network/libs/cloudinit.py index 358ab8c7cb..0c442f2a26 100644 --- a/tests/network/libs/cloudinit.py +++ b/tests/network/libs/cloudinit.py @@ -3,8 +3,8 @@ import yaml +from libs.net.apimachinery import dict_normalization_for_dataclass from libs.net.cluster import ipv4_supported_cluster, ipv6_supported_cluster -from tests.network.libs.apimachinery import dict_normalization_for_dataclass NETWORK_DATA: Final[str] = "networkData" diff --git a/tests/network/libs/cluster_user_defined_network.py b/tests/network/libs/cluster_user_defined_network.py index 390b28dc79..0a11b60725 100644 --- a/tests/network/libs/cluster_user_defined_network.py +++ b/tests/network/libs/cluster_user_defined_network.py @@ -4,7 +4,7 @@ from kubernetes.dynamic import DynamicClient from ocp_resources.cluster_user_defined_network import ClusterUserDefinedNetwork as Cudn -from tests.network.libs.apimachinery import dict_normalization_for_dataclass +from libs.net.apimachinery import dict_normalization_for_dataclass from tests.network.libs.label_selector import LabelSelector diff --git a/tests/network/localnet/conftest.py b/tests/network/localnet/conftest.py index 638da8f976..d2e8f41bd0 100644 --- a/tests/network/localnet/conftest.py +++ b/tests/network/localnet/conftest.py @@ -4,7 +4,7 @@ from kubernetes.dynamic import DynamicClient from ocp_resources.namespace import Namespace -import tests.network.libs.nodenetworkconfigurationpolicy as libnncp +from libs.net import nodenetworkconfigurationpolicy as libnncp 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 TcpServer, VMTcpClient, active_tcp_connections diff --git a/tests/network/localnet/ipam/conftest.py b/tests/network/localnet/ipam/conftest.py index 6ffc7b1f5f..af5b53061e 100644 --- a/tests/network/localnet/ipam/conftest.py +++ b/tests/network/localnet/ipam/conftest.py @@ -4,8 +4,8 @@ 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 import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import random_ipv4_address from libs.vm.oper import run_vms from libs.vm.spec import Interface, Multus, Network diff --git a/tests/network/localnet/liblocalnet.py b/tests/network/localnet/liblocalnet.py index aef40d037e..4f41159b7e 100644 --- a/tests/network/localnet/liblocalnet.py +++ b/tests/network/localnet/liblocalnet.py @@ -7,6 +7,7 @@ from kubernetes.dynamic import DynamicClient +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.cluster import ipv4_supported_cluster, ipv6_supported_cluster from libs.vm.affinity import new_pod_anti_affinity from libs.vm.factory import base_vmspec, fedora_vm @@ -14,7 +15,6 @@ from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage from tests.network.libs import cloudinit from tests.network.libs import cluster_user_defined_network as libcudn -from tests.network.libs import nodenetworkconfigurationpolicy as libnncp from tests.network.libs.label_selector import LabelSelector from utilities.constants import OVS_BRIDGE, WORKER_NODE_LABEL_KEY From a383cdff401fb8f12bf30e9c83c514279b14fa12 Mon Sep 17 00:00:00 2001 From: Asia Khromov Date: Sun, 31 May 2026 12:39:39 +0300 Subject: [PATCH 2/7] net, tests: Add cluster_vlan_iterator and root fixture Add cluster_vlan_iterator() to libs/net/cluster.py. The underlying VLAN list is read from py_config once via a cached helper; each call returns a fresh generator so every fixture invocation starts from the beginning. Add next_vlan_index_number fixture to tests/conftest.py using this helper so any test suite can allocate cluster VLANs without cross-package imports. Network tests continue to use their own fixture in tests/network/conftest.py and will be adjusted in a follow-up. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Asia Khromov --- libs/net/cluster.py | 21 +++++++++++++++++++++ tests/conftest.py | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/libs/net/cluster.py b/libs/net/cluster.py index 06522d47e1..1f0752a17d 100644 --- a/libs/net/cluster.py +++ b/libs/net/cluster.py @@ -1,5 +1,6 @@ import ipaddress import logging +from collections.abc import Generator from functools import cache from pytest_testconfig import py_config @@ -29,3 +30,23 @@ def ipv6_supported_cluster() -> bool: def _cluster_ip_family_supported(ip_family: int) -> bool: return any(ipaddress.ip_network(ip).version == ip_family for ip in py_config.get("cluster_service_network")) + + +def cluster_vlan_iterator() -> Generator[int]: + """Yield VLAN IDs from the cluster config one at a time. + + The underlying VLAN list is read from py_config once and cached. Each call + returns a fresh iterator so every fixture invocation starts from the beginning. + Raises ValueError when all VLANs have been consumed. + """ + vlans = _cluster_vlans() + yield from vlans + raise ValueError(f"vlans list is exhausted. Current list size is {len(vlans)} and all vlans are in use.") + + +@cache +def _cluster_vlans() -> list[int]: + vlans = py_config["vlans"] + if not isinstance(vlans, list): + vlans = vlans.split(",") + return [int(v) for v in vlans] diff --git a/tests/conftest.py b/tests/conftest.py index 7cc78751dc..2345a06cfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,7 +69,7 @@ from timeout_sampler import TimeoutSampler import utilities.hco -from libs.net.cluster import ipv4_supported_cluster, ipv6_supported_cluster +from libs.net.cluster import cluster_vlan_iterator, ipv4_supported_cluster, ipv6_supported_cluster from libs.net.ip import filter_link_local_addresses, random_ipv4_address, random_ipv6_address from libs.net.vmspec import lookup_iface_status from tests.utils import download_and_extract_tar @@ -2734,3 +2734,8 @@ def hugepages_gib_values(workers): for worker in workers if (value := worker.instance.status.allocatable.get(NODE_HUGE_PAGES_1GI_KEY)) ] + + +@pytest.fixture(scope="module") +def next_vlan_index_number(): # skip-unused-code + return cluster_vlan_iterator() From fcc50e711b6393bae1924f1d47fbb80f9f0cc080 Mon Sep 17 00:00:00 2001 From: Asia Khromov Date: Sun, 31 May 2026 12:39:39 +0300 Subject: [PATCH 3/7] net: Widen vm param type in VMI condition-wait helpers Wait condition helpers use vm.vmi which exists on both BaseVirtualMachine and VirtualMachineForTests. Widen the type to VirtualMachine so any subclass can be passed without a type error. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Asia Khromov --- libs/net/vmspec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/net/vmspec.py b/libs/net/vmspec.py index adff3acf3d..c78527a621 100644 --- a/libs/net/vmspec.py +++ b/libs/net/vmspec.py @@ -206,7 +206,7 @@ def wait_for_ifaces_status( def wait_for_vmi_condition_status( - vm: BaseVirtualMachine, + vm: VirtualMachine, condition: str, status: str = ResourceConstants.Condition.Status.TRUE, timeout: int = 300, @@ -249,7 +249,7 @@ def wait_for_vmi_condition_status( def wait_for_no_vmi_condition( - vm: BaseVirtualMachine, + vm: VirtualMachine, condition: str, timeout: int = 300, resource_version: str | None = None, From 0be6484e85792ddf9ed2824b1ff35ba8febba5d7 Mon Sep 17 00:00:00 2001 From: Ohad Date: Tue, 2 Jun 2026 14:22:13 +0300 Subject: [PATCH 4/7] obs: Add vnic_info metric test for NAD reference change Implement test_metric_kubevirt_vm_vnic_info_after_nad_swap to verify that kubevirt_vm_vnic_info and kubevirt_vmi_vnic_info metric labels update correctly after swapping the VM secondary network NAD reference and completing the triggered live migration. Signed-off-by: Ohad Assisted-by: claude code claude-opus-4-6 --- tests/conftest.py | 2 +- tests/observability/metrics/conftest.py | 132 ++++++++++++++++++ .../observability/metrics/test_vms_metrics.py | 17 ++- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2345a06cfe..7d4ca38e4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2737,5 +2737,5 @@ def hugepages_gib_values(workers): @pytest.fixture(scope="module") -def next_vlan_index_number(): # skip-unused-code +def next_vlan_index_number(): return cluster_vlan_iterator() diff --git a/tests/observability/metrics/conftest.py b/tests/observability/metrics/conftest.py index 4330d744dd..5c73fd2501 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -1,5 +1,6 @@ import logging import shlex +from collections.abc import Generator import bitmath import pytest @@ -15,7 +16,12 @@ from pytest_testconfig import py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler +import libs.net.nodenetworkconfigurationpolicy as libnncp +from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition +from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status from tests.observability.metrics.constants import ( + BINDING_NAME, + BINDING_TYPE, GUEST_LOAD_TIME_PERIODS, KUBEVIRT_CONSOLE_ACTIVE_CONNECTIONS_BY_VMI, KUBEVIRT_VM_CREATED_BY_POD_TOTAL, @@ -27,6 +33,7 @@ ) from tests.observability.metrics.utils import ( SINGLE_VM, + binding_name_and_type_from_vm_or_vmi, create_windows11_wsl2_vm, disk_file_system_info, enable_swap_fedora_vm, @@ -47,6 +54,7 @@ KUBEVIRT_VMI_MEMORY_SWAP_OUT_TRAFFIC_BYTES, KUBEVIRT_VMI_MEMORY_UNUSED_BYTES, KUBEVIRT_VMI_MEMORY_USABLE_BYTES, + LINUX_BRIDGE, MIGRATION_POLICY_VM_LABEL, ONE_CPU_CORE, ONE_CPU_THREAD, @@ -63,6 +71,7 @@ TWO_CPU_THREADS, U1_MEDIUM_STR, VIRT_TEMPLATE_VALIDATOR, + WORKER_NODE_LABEL_KEY, Images, ) from utilities.hco import ResourceEditorValidateHCOReconcile, enabled_aaq_in_hco @@ -661,3 +670,126 @@ def expected_cpu_affinity_metric_value(admin_client, vm_with_cpu_spec): # return multiplication for multi-CPU VMs return str(cpu_count_from_vm_node * cpu_count_from_vm) + + +_NAD_SWAP_BRIDGE_NAME = "brnadswap" +_NAD_SWAP_SECONDARY_IFACE = "secondary" + + +@pytest.fixture(scope="class") +def nad_swap_bridge_nncp( + nmstate_dependent_placeholder, + admin_client, + hosts_common_available_ports, +) -> Generator[libnncp.NodeNetworkConfigurationPolicy]: + with libnncp.NodeNetworkConfigurationPolicy( + client=admin_client, + name="nad-swap-vnic-info-nncp", + desired_state=libnncp.DesiredState( + interfaces=[ + libnncp.Interface( + name=_NAD_SWAP_BRIDGE_NAME, + type=LINUX_BRIDGE, + state=libnncp.Resource.Interface.State.UP, + bridge=libnncp.Bridge( + port=[libnncp.Port(name=hosts_common_available_ports[-1])], + options=libnncp.BridgeOptions(libnncp.STP(enabled=False)), + ), + ) + ] + ), + node_selector={WORKER_NODE_LABEL_KEY: ""}, + ) as nncp: + nncp.wait_for_status_success() + yield nncp + + +@pytest.fixture(scope="class") +def nad_a_for_vnic_info( + admin_client, + namespace, + nad_swap_bridge_nncp, + next_vlan_index_number, +) -> Generator[NetworkAttachmentDefinition]: + with NetworkAttachmentDefinition( + name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-a", + namespace=namespace.name, + config=NetConfig( + name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-a", + plugins=[CNIPluginBridgeConfig(bridge=_NAD_SWAP_BRIDGE_NAME, vlan=next(next_vlan_index_number))], + ), + client=admin_client, + ) as nad: + yield nad + + +@pytest.fixture(scope="class") +def nad_b_for_vnic_info( + admin_client, + namespace, + nad_swap_bridge_nncp, + next_vlan_index_number, +) -> Generator[NetworkAttachmentDefinition]: + with NetworkAttachmentDefinition( + name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b", + namespace=namespace.name, + config=NetConfig( + name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b", + plugins=[CNIPluginBridgeConfig(bridge=_NAD_SWAP_BRIDGE_NAME, vlan=next(next_vlan_index_number))], + ), + client=admin_client, + ) as nad: + yield nad + + +@pytest.fixture(scope="class") +def vm_for_nad_swap_test( + unprivileged_client, + namespace, + nad_a_for_vnic_info, +) -> Generator[VirtualMachineForTests]: + vm_name = "vm-nad-swap-vnic-info" + with VirtualMachineForTests( + name=vm_name, + namespace=namespace.name, + body=fedora_vm_body(name=vm_name), + networks={_NAD_SWAP_SECONDARY_IFACE: nad_a_for_vnic_info.name}, + interfaces=[_NAD_SWAP_SECONDARY_IFACE], + client=unprivileged_client, + ) as vm: + running_vm(vm=vm, wait_for_cloud_init=False) + yield vm + + +@pytest.fixture(scope="class") +def post_nad_swap_vm( + vm_for_nad_swap_test, + nad_b_for_vnic_info, +) -> Generator[VirtualMachineForTests]: + resource_version = vm_for_nad_swap_test.vmi.instance.metadata.resourceVersion + networks = list(vm_for_nad_swap_test.instance.spec.template.spec.networks) + for network in networks: + if network["name"] == _NAD_SWAP_SECONDARY_IFACE: + network["multus"].update({"networkName": nad_b_for_vnic_info.name}) + ResourceEditor(patches={vm_for_nad_swap_test: {"spec": {"template": {"spec": {"networks": networks}}}}}).update() + wait_for_vmi_condition_status( + vm=vm_for_nad_swap_test, condition="MigrationRequired", resource_version=resource_version + ) + wait_for_no_vmi_condition(vm=vm_for_nad_swap_test, condition="MigrationRequired") + yield vm_for_nad_swap_test + + +@pytest.fixture(scope="class") +def expected_vnic_info_after_swap( + post_nad_swap_vm, + nad_b_for_vnic_info, +) -> dict[str, str]: + vm_interfaces = post_nad_swap_vm.instance.spec.template.spec.domain.devices.interfaces + secondary_interface = next(iface for iface in vm_interfaces if iface["name"] == _NAD_SWAP_SECONDARY_IFACE) + binding_info = binding_name_and_type_from_vm_or_vmi(vm_interface=secondary_interface) + return { + "vnic_name": _NAD_SWAP_SECONDARY_IFACE, + BINDING_NAME: binding_info[BINDING_NAME], + BINDING_TYPE: binding_info[BINDING_TYPE], + "network": nad_b_for_vnic_info.name, + } diff --git a/tests/observability/metrics/test_vms_metrics.py b/tests/observability/metrics/test_vms_metrics.py index fb4368c33d..fc601df3d3 100644 --- a/tests/observability/metrics/test_vms_metrics.py +++ b/tests/observability/metrics/test_vms_metrics.py @@ -463,7 +463,9 @@ def test_metric_kubevirt_vmi_vnic_info_windows(self, prometheus, windows_vm_for_ ), ], ) - def test_metric_kubevirt_vm_vnic_info_after_nad_swap(self, query): + def test_metric_kubevirt_vm_vnic_info_after_nad_swap( + self, prometheus, post_nad_swap_vm, expected_vnic_info_after_swap, query + ): """ Test that vnic_info metric updates the network label after a NAD swap. @@ -472,17 +474,20 @@ def test_metric_kubevirt_vm_vnic_info_after_nad_swap(self, query): Preconditions: - Two Network Attachment Definitions (NAD-A, NAD-B) with different VLANs on the same Linux bridge - - Running VM with a secondary bridge interface attached to NAD-A + - Running VM with a secondary bridge interface whose reference was swapped from NAD-A to NAD-B + and the triggered live migration has completed Steps: - 1. Swap the VM secondary network reference from NAD-A to NAD-B - 2. Query vnic_info metric for the secondary interface + 1. Query vnic_info metric for the secondary interface Expected: - vnic_info labels match the VM spec after NAD swap """ - - test_metric_kubevirt_vm_vnic_info_after_nad_swap.__test__ = False + validate_vnic_info( + prometheus=prometheus, + vnic_info_to_compare=expected_vnic_info_after_swap, + metric_name=query.format(vm_name=post_nad_swap_vm.name), + ) class TestVmiPhaseTransitionFromDeletion: From a7fb8e7b4d3ab46ed5a926c9f3a13ebb0581860e Mon Sep 17 00:00:00 2001 From: Ohad Date: Mon, 8 Jun 2026 09:28:04 +0300 Subject: [PATCH 5/7] obs: Address review comments for NAD swap vnic_info test - Use BaseVirtualMachine via base_vmspec/fedora_vm for vm_for_nad_swap_test - Reuse update_nad_references from tests/utils.py in post_nad_swap_vm - Fix validate_vnic_info to poll until metric matches expected values instead of breaking on the first (possibly stale) result - Fix import style per AGENTS.md (from libs.net import ... as libnncp) - Restore original STD steps in test docstring Signed-off-by: Ohad Assisted-by: claude code claude-opus-4-6 --- tests/observability/metrics/conftest.py | 49 +++++++++---------- .../observability/metrics/test_vms_metrics.py | 7 +-- tests/observability/metrics/utils.py | 18 +++---- tests/utils.py | 23 +++++++++ 4 files changed, 60 insertions(+), 37 deletions(-) diff --git a/tests/observability/metrics/conftest.py b/tests/observability/metrics/conftest.py index 5c73fd2501..4ef16ac061 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -16,9 +16,10 @@ from pytest_testconfig import py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler -import libs.net.nodenetworkconfigurationpolicy as libnncp +from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition -from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status +from libs.vm.factory import base_vmspec, fedora_vm +from libs.vm.spec import Devices, Interface, Multus, Network from tests.observability.metrics.constants import ( BINDING_NAME, BINDING_TYPE, @@ -44,7 +45,7 @@ vnic_info_from_vm_or_vmi, ) from tests.observability.utils import validate_metrics_value -from tests.utils import create_vms, start_stress_on_vm +from tests.utils import create_vms, start_stress_on_vm, update_nad_references from utilities import console from utilities.constants import ( IPV4_STR, @@ -729,7 +730,7 @@ def nad_b_for_vnic_info( namespace, nad_swap_bridge_nncp, next_vlan_index_number, -) -> Generator[NetworkAttachmentDefinition]: +): with NetworkAttachmentDefinition( name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b", namespace=namespace.name, @@ -747,17 +748,21 @@ def vm_for_nad_swap_test( unprivileged_client, namespace, nad_a_for_vnic_info, -) -> Generator[VirtualMachineForTests]: +): vm_name = "vm-nad-swap-vnic-info" - with VirtualMachineForTests( - name=vm_name, - namespace=namespace.name, - body=fedora_vm_body(name=vm_name), - networks={_NAD_SWAP_SECONDARY_IFACE: nad_a_for_vnic_info.name}, - interfaces=[_NAD_SWAP_SECONDARY_IFACE], - client=unprivileged_client, - ) as vm: - running_vm(vm=vm, wait_for_cloud_init=False) + spec = base_vmspec() + spec.template.spec.domain.devices = Devices( + interfaces=[ + Interface(name="default", masquerade={}), + Interface(name=_NAD_SWAP_SECONDARY_IFACE, bridge={}), + ] + ) + spec.template.spec.networks = [ + Network(name="default", pod={}), + Network(name=_NAD_SWAP_SECONDARY_IFACE, multus=Multus(networkName=nad_a_for_vnic_info.name)), + ] + with fedora_vm(namespace=namespace.name, name=vm_name, client=unprivileged_client, spec=spec) as vm: + vm.start(wait=True) yield vm @@ -765,17 +770,11 @@ def vm_for_nad_swap_test( def post_nad_swap_vm( vm_for_nad_swap_test, nad_b_for_vnic_info, -) -> Generator[VirtualMachineForTests]: - resource_version = vm_for_nad_swap_test.vmi.instance.metadata.resourceVersion - networks = list(vm_for_nad_swap_test.instance.spec.template.spec.networks) - for network in networks: - if network["name"] == _NAD_SWAP_SECONDARY_IFACE: - network["multus"].update({"networkName": nad_b_for_vnic_info.name}) - ResourceEditor(patches={vm_for_nad_swap_test: {"spec": {"template": {"spec": {"networks": networks}}}}}).update() - wait_for_vmi_condition_status( - vm=vm_for_nad_swap_test, condition="MigrationRequired", resource_version=resource_version +): + update_nad_references( + vm=vm_for_nad_swap_test, + nad_name_by_net={_NAD_SWAP_SECONDARY_IFACE: nad_b_for_vnic_info.name}, ) - wait_for_no_vmi_condition(vm=vm_for_nad_swap_test, condition="MigrationRequired") yield vm_for_nad_swap_test @@ -783,7 +782,7 @@ def post_nad_swap_vm( def expected_vnic_info_after_swap( post_nad_swap_vm, nad_b_for_vnic_info, -) -> dict[str, str]: +): vm_interfaces = post_nad_swap_vm.instance.spec.template.spec.domain.devices.interfaces secondary_interface = next(iface for iface in vm_interfaces if iface["name"] == _NAD_SWAP_SECONDARY_IFACE) binding_info = binding_name_and_type_from_vm_or_vmi(vm_interface=secondary_interface) diff --git a/tests/observability/metrics/test_vms_metrics.py b/tests/observability/metrics/test_vms_metrics.py index fc601df3d3..693665ae66 100644 --- a/tests/observability/metrics/test_vms_metrics.py +++ b/tests/observability/metrics/test_vms_metrics.py @@ -474,11 +474,12 @@ def test_metric_kubevirt_vm_vnic_info_after_nad_swap( Preconditions: - Two Network Attachment Definitions (NAD-A, NAD-B) with different VLANs on the same Linux bridge - - Running VM with a secondary bridge interface whose reference was swapped from NAD-A to NAD-B - and the triggered live migration has completed + - Running VM with a secondary bridge interface attached to NAD-A Steps: - 1. Query vnic_info metric for the secondary interface + 1. Swap the VM secondary network reference from NAD-A to NAD-B + 2. Wait for the live migration triggered by the swap to complete + 3. Query vnic_info metric for the secondary interface Expected: - vnic_info labels match the VM spec after NAD swap diff --git a/tests/observability/metrics/utils.py b/tests/observability/metrics/utils.py index 7f8b1c9a6a..baa2ebf0be 100644 --- a/tests/observability/metrics/utils.py +++ b/tests/observability/metrics/utils.py @@ -596,21 +596,21 @@ def validate_vnic_info(prometheus: Prometheus, vnic_info_to_compare: dict[str, s func=prometheus.query, query=metric_name, ) - sample = None + mismatch_vnic_info = None try: for sample in samples: if sample and (result := sample.get("data", {}).get("result")): vnic_info_metric_result = result[0].get("metric") - break + mismatch_vnic_info = {} + for info, expected_value in vnic_info_to_compare.items(): + actual_value = vnic_info_metric_result.get(info) + if actual_value != expected_value: + mismatch_vnic_info[info] = {f"Expected: {expected_value}", f"Actual: {actual_value}"} + if not mismatch_vnic_info: + return except TimeoutExpiredError: - LOGGER.error(f"Metric value of: {metric_name} is: {sample}, should not be empty.") + LOGGER.error(f"There is a mismatch between expected and actual results:\n {mismatch_vnic_info}") raise - mismatch_vnic_info = {} - for info, expected_value in vnic_info_to_compare.items(): - actual_value = vnic_info_metric_result.get(info) - if actual_value != expected_value: - mismatch_vnic_info[info] = {f"Expected: {expected_value}", f"Actual: {actual_value}"} - assert not mismatch_vnic_info, f"There is a mismatch between expected and actual results:\n {mismatch_vnic_info}" def get_metric_labels_non_empty_value(prometheus: Prometheus, metric_name: str) -> dict[str, str]: diff --git a/tests/utils.py b/tests/utils.py index 1ab4742c0e..e7a671923b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,6 +5,7 @@ import shlex import tarfile from contextlib import contextmanager +from copy import deepcopy from io import BytesIO from typing import Generator, Optional @@ -26,6 +27,8 @@ from pytest_testconfig import config as py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler, retry +from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status +from libs.vm.vm import BaseVirtualMachine from utilities.artifactory import ( cleanup_artifactory_secret_and_config_map, get_artifactory_config_map, @@ -779,3 +782,23 @@ def create_windows2022_vm_with_vtpm_from_registry( running_vm(vm=vm) wait_for_windows_vm(vm=vm, version="2022") yield vm + + +def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: + """Update secondary network NAD references and wait for the change to be fully applied. + + Patches the VM spec atomically, then waits for the MigrationRequired condition to + appear (change detected) and disappear (migration completed). + + Args: + vm: The virtual machine to update. + nad_name_by_net: Mapping of interface name to new NAD name. + """ + resource_version = vm.vmi.instance.metadata.resourceVersion + networks = deepcopy(vm.template_spec.networks) or [] + for network in networks: + if network.name in nad_name_by_net and network.multus: + network.multus.networkName = nad_name_by_net[network.name] + vm.set_networks(networks=networks) + wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) + wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") From b31abcc10f360c7416ed3816fce5ca18f1df9366 Mon Sep 17 00:00:00 2001 From: Ohad Date: Tue, 9 Jun 2026 13:03:34 +0300 Subject: [PATCH 6/7] obs, net: Deduplicate update_nad_references into libs/net/vmspec Move update_nad_references to libs/net/vmspec.py to eliminate identical copies in tests/utils.py and tests/network/l2_bridge/nad_ref_change/lib_helpers.py. The function is used by both network and observability team directories, so it belongs in a shared module per project conventions. assisted by: claude code claude-opus-4-6 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ohad --- libs/net/vmspec.py | 21 +++++++++++++++++ .../l2_bridge/nad_ref_change/lib_helpers.py | 22 ------------------ .../nad_ref_change/test_nad_ref_change.py | 3 +-- tests/observability/metrics/conftest.py | 3 ++- tests/utils.py | 23 ------------------- 5 files changed, 24 insertions(+), 48 deletions(-) diff --git a/libs/net/vmspec.py b/libs/net/vmspec.py index c78527a621..c6e149389b 100644 --- a/libs/net/vmspec.py +++ b/libs/net/vmspec.py @@ -1,5 +1,6 @@ import ipaddress from collections.abc import Callable +from copy import deepcopy from typing import Any, Final from kubernetes.dynamic.client import ResourceField @@ -299,3 +300,23 @@ def _vmi_condition_not_set(existing_conditions: list[ResourceField], required_co for cond in existing_conditions if cond.type == required_condition ) + + +def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: + """Update secondary network NAD references and wait for the change to be fully applied. + + Patches the VM spec atomically, then waits for the MigrationRequired condition to + appear (change detected) and disappear (migration completed). + + Args: + vm: The virtual machine to update. + nad_name_by_net: Mapping of interface name to new NAD name. + """ + resource_version = vm.vmi.instance.metadata.resourceVersion + networks = deepcopy(vm.template_spec.networks) or [] + for network in networks: + if network.name in nad_name_by_net and network.multus: + network.multus.networkName = nad_name_by_net[network.name] + vm.set_networks(networks=networks) + wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) + wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") diff --git a/tests/network/l2_bridge/nad_ref_change/lib_helpers.py b/tests/network/l2_bridge/nad_ref_change/lib_helpers.py index b1b7cbf648..ff4bdf9d03 100644 --- a/tests/network/l2_bridge/nad_ref_change/lib_helpers.py +++ b/tests/network/l2_bridge/nad_ref_change/lib_helpers.py @@ -1,9 +1,7 @@ -from copy import deepcopy from typing import Final from kubernetes.dynamic import DynamicClient -from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status from libs.vm.factory import base_vmspec, fedora_vm from libs.vm.spec import ( CloudInitNoCloud, @@ -75,26 +73,6 @@ def assert_no_connectivity( ) -def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: - """Update secondary network NAD references and wait for the change to be fully applied. - - Patches the VM spec atomically, then waits for the MigrationRequired condition to - appear (change detected) and disappear (migration completed). - - Args: - vm: The virtual machine to update. - nad_name_by_net: Mapping of interface name to new NAD name. - """ - resource_version = vm.vmi.instance.metadata.resourceVersion - networks = deepcopy(vm.template_spec.networks) or [] - for network in networks: - if network.name in nad_name_by_net and network.multus: - network.multus.networkName = nad_name_by_net[network.name] - vm.set_networks(networks=networks) - wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) - wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") - - def two_secondary_bridge_vm( namespace: str, name: str, diff --git a/tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py b/tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py index e2843ae558..f7aceae809 100644 --- a/tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py +++ b/tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py @@ -14,14 +14,13 @@ import pytest from libs.net.ip import filter_link_local_addresses -from libs.net.vmspec import lookup_iface_status +from libs.net.vmspec import lookup_iface_status, update_nad_references from tests.network.l2_bridge.libl2bridge import LINUX_BRIDGE_IFACE_NAME_1, LINUX_BRIDGE_IFACE_NAME_2 from tests.network.l2_bridge.nad_ref_change.lib_helpers import ( GUEST_IFACE_1, GUEST_IFACE_2, assert_connectivity, assert_no_connectivity, - update_nad_references, ) diff --git a/tests/observability/metrics/conftest.py b/tests/observability/metrics/conftest.py index 4ef16ac061..6d567b6841 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -18,6 +18,7 @@ from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition +from libs.net.vmspec import update_nad_references from libs.vm.factory import base_vmspec, fedora_vm from libs.vm.spec import Devices, Interface, Multus, Network from tests.observability.metrics.constants import ( @@ -45,7 +46,7 @@ vnic_info_from_vm_or_vmi, ) from tests.observability.utils import validate_metrics_value -from tests.utils import create_vms, start_stress_on_vm, update_nad_references +from tests.utils import create_vms, start_stress_on_vm from utilities import console from utilities.constants import ( IPV4_STR, diff --git a/tests/utils.py b/tests/utils.py index e7a671923b..1ab4742c0e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,6 @@ import shlex import tarfile from contextlib import contextmanager -from copy import deepcopy from io import BytesIO from typing import Generator, Optional @@ -27,8 +26,6 @@ from pytest_testconfig import config as py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler, retry -from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status -from libs.vm.vm import BaseVirtualMachine from utilities.artifactory import ( cleanup_artifactory_secret_and_config_map, get_artifactory_config_map, @@ -782,23 +779,3 @@ def create_windows2022_vm_with_vtpm_from_registry( running_vm(vm=vm) wait_for_windows_vm(vm=vm, version="2022") yield vm - - -def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: - """Update secondary network NAD references and wait for the change to be fully applied. - - Patches the VM spec atomically, then waits for the MigrationRequired condition to - appear (change detected) and disappear (migration completed). - - Args: - vm: The virtual machine to update. - nad_name_by_net: Mapping of interface name to new NAD name. - """ - resource_version = vm.vmi.instance.metadata.resourceVersion - networks = deepcopy(vm.template_spec.networks) or [] - for network in networks: - if network.name in nad_name_by_net and network.multus: - network.multus.networkName = nad_name_by_net[network.name] - vm.set_networks(networks=networks) - wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) - wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") From 30dbdcd6507f995f4192e44a9702c3e61577cd4c Mon Sep 17 00:00:00 2001 From: Ohad Date: Wed, 10 Jun 2026 08:59:18 +0300 Subject: [PATCH 7/7] obs, net: Deduplicate update_nad_references into libs/net/vmspec Move update_nad_references to libs/net/vmspec.py to eliminate identical copies in tests/utils.py, tests/network/l2_bridge/nad_ref_change/lib_helpers.py, and tests/network/libs/nad_ref.py. The function is used across network and observability team directories, so it belongs in a shared module. Also adds fail-fast validation: raises ValueError for missing networks, non-Multus networks, and unmatched interface names. assisted by: claude code claude-opus-4-6 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ohad --- libs/net/cluster.py | 21 ----- libs/net/vmspec.py | 18 +++- tests/conftest.py | 88 ++++++++++++++++++- tests/network/conftest.py | 15 ---- tests/network/l2_bridge/conftest.py | 29 ------ .../l2_bridge/nad_ref_change/conftest.py | 41 +-------- tests/network/libs/nad_ref.py | 24 ----- .../nad_ref_change/test_nad_ref_change.py | 3 +- tests/network/utils.py | 5 -- tests/observability/metrics/conftest.py | 84 ++---------------- tests/utils.py | 5 ++ 11 files changed, 112 insertions(+), 221 deletions(-) delete mode 100644 tests/network/libs/nad_ref.py diff --git a/libs/net/cluster.py b/libs/net/cluster.py index 1f0752a17d..06522d47e1 100644 --- a/libs/net/cluster.py +++ b/libs/net/cluster.py @@ -1,6 +1,5 @@ import ipaddress import logging -from collections.abc import Generator from functools import cache from pytest_testconfig import py_config @@ -30,23 +29,3 @@ def ipv6_supported_cluster() -> bool: def _cluster_ip_family_supported(ip_family: int) -> bool: return any(ipaddress.ip_network(ip).version == ip_family for ip in py_config.get("cluster_service_network")) - - -def cluster_vlan_iterator() -> Generator[int]: - """Yield VLAN IDs from the cluster config one at a time. - - The underlying VLAN list is read from py_config once and cached. Each call - returns a fresh iterator so every fixture invocation starts from the beginning. - Raises ValueError when all VLANs have been consumed. - """ - vlans = _cluster_vlans() - yield from vlans - raise ValueError(f"vlans list is exhausted. Current list size is {len(vlans)} and all vlans are in use.") - - -@cache -def _cluster_vlans() -> list[int]: - vlans = py_config["vlans"] - if not isinstance(vlans, list): - vlans = vlans.split(",") - return [int(v) for v in vlans] diff --git a/libs/net/vmspec.py b/libs/net/vmspec.py index c6e149389b..5c1dea768e 100644 --- a/libs/net/vmspec.py +++ b/libs/net/vmspec.py @@ -303,7 +303,7 @@ def _vmi_condition_not_set(existing_conditions: list[ResourceField], required_co def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: - """Update secondary network NAD references and wait for the change to be fully applied. + """Update secondary NAD references and wait for the change to be fully applied. Patches the VM spec atomically, then waits for the MigrationRequired condition to appear (change detected) and disappear (migration completed). @@ -312,11 +312,23 @@ def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str vm: The virtual machine to update. nad_name_by_net: Mapping of interface name to new NAD name. """ + if not nad_name_by_net: + raise ValueError(f"NAD update mapping is empty for VM {vm.name}") resource_version = vm.vmi.instance.metadata.resourceVersion - networks = deepcopy(vm.template_spec.networks) or [] + networks = vm.template_spec.networks + if not networks: + raise ValueError(f"VM {vm.name} has no template networks to update") + networks = deepcopy(networks) + updated_names = set() for network in networks: - if network.name in nad_name_by_net and network.multus: + if network.name in nad_name_by_net: + if not network.multus: + raise ValueError(f"Network {network.name!r} on VM {vm.name} is not a Multus network") network.multus.networkName = nad_name_by_net[network.name] + updated_names.add(network.name) + missing = set(nad_name_by_net) - updated_names + if missing: + raise ValueError(f"NAD update requested for unknown networks {sorted(missing)} on VM {vm.name}") vm.set_networks(networks=networks) wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") diff --git a/tests/conftest.py b/tests/conftest.py index 78f51baf80..880baaaffd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ import tempfile from bisect import bisect_left from collections import defaultdict +from collections.abc import Generator from datetime import UTC, datetime from signal import SIGINT, SIGTERM, getsignal, signal @@ -23,6 +24,7 @@ import requests import yaml from bs4 import BeautifulSoup +from kubernetes.dynamic import DynamicClient from kubernetes.dynamic.exceptions import ResourceNotFoundError from ocp_resources.application_aware_resource_quota import ApplicationAwareResourceQuota from ocp_resources.catalog_source import CatalogSource @@ -69,10 +71,12 @@ from timeout_sampler import TimeoutSampler import utilities.hco -from libs.net.cluster import cluster_vlan_iterator, ipv4_supported_cluster, ipv6_supported_cluster +from libs.net import nodenetworkconfigurationpolicy as libnncp +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.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition from libs.net.vmspec import lookup_iface_status -from tests.utils import download_and_extract_tar +from tests.utils import download_and_extract_tar, get_vlan_index_number from utilities.artifactory import get_artifactory_header, get_http_image_url, get_test_artifact_server_url from utilities.bitwarden import get_cnv_tests_secret_by_name from utilities.cluster import cache_admin_client, get_oc_whoami_username @@ -2741,6 +2745,82 @@ def hugepages_gib_values(workers): ] +@pytest.fixture(scope="package") +def bridge_nncp( + nmstate_dependent_placeholder: None, + admin_client: DynamicClient, + hosts_common_available_ports: list[str], +) -> Generator[libnncp.NodeNetworkConfigurationPolicy]: + if not hosts_common_available_ports: + raise ValueError("No common worker NICs available for bridge_nncp fixture") + with libnncp.NodeNetworkConfigurationPolicy( + client=admin_client, + name="l2-bridge-test-nncp", + desired_state=libnncp.DesiredState( + interfaces=[ + libnncp.Interface( + name="br1-test", + type=LINUX_BRIDGE, + state=libnncp.Resource.Interface.State.UP, + bridge=libnncp.Bridge( + port=[libnncp.Port(name=hosts_common_available_ports[-1])], + options=libnncp.BridgeOptions(libnncp.STP(enabled=False)), + ), + ) + ] + ), + node_selector={WORKER_NODE_LABEL_KEY: ""}, + ) as nncp_br: + nncp_br.wait_for_status_success() + yield nncp_br + + @pytest.fixture(scope="module") -def next_vlan_index_number(): - return cluster_vlan_iterator() +def bridge_nad_a( + admin_client: DynamicClient, + namespace: Namespace, + bridge_nncp: libnncp.NodeNetworkConfigurationPolicy, + vlan_index_number: Generator[int], +) -> Generator[NetworkAttachmentDefinition]: + bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore + with NetworkAttachmentDefinition( + name="nad-vlan-a", + namespace=namespace.name, + config=NetConfig( + name="nad-vlan-a", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))] + ), + client=admin_client, + ) as nad: + yield nad + + +@pytest.fixture(scope="module") +def vlan_index_number(vlans_list): + return get_vlan_index_number(vlans_list=vlans_list) + + +@pytest.fixture(scope="session") +def vlans_list(): + vlans = py_config["vlans"] + if not isinstance(vlans, list): + vlans = vlans.split(",") + return [int(_id) for _id in vlans] + + +@pytest.fixture(scope="module") +def bridge_nad_b( + admin_client: DynamicClient, + namespace: Namespace, + bridge_nncp: libnncp.NodeNetworkConfigurationPolicy, + vlan_index_number: Generator[int], +) -> Generator[NetworkAttachmentDefinition]: + bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore[union-attr, index] + with NetworkAttachmentDefinition( + name="nad-vlan-b", + namespace=namespace.name, + config=NetConfig( + name="nad-vlan-b", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))] + ), + client=admin_client, + ) as nad: + yield nad diff --git a/tests/network/conftest.py b/tests/network/conftest.py index dd5617e262..a5e3d2664f 100644 --- a/tests/network/conftest.py +++ b/tests/network/conftest.py @@ -11,11 +11,9 @@ from ocp_resources.network_config_openshift_io import Network from ocp_resources.performance_profile import PerformanceProfile from ocp_resources.pod import Pod -from pytest_testconfig import config as py_config from timeout_sampler import TimeoutExpiredError from libs.net.cluster import ipv4_supported_cluster, ipv6_supported_cluster -from tests.network.utils import get_vlan_index_number from utilities.constants import ( CLUSTER, CLUSTER_NETWORK_ADDONS_OPERATOR, @@ -95,19 +93,6 @@ def sriov_workers_node2(sriov_workers): return sriov_workers[1] -@pytest.fixture(scope="session") -def vlans_list(): - vlans = py_config["vlans"] - if not isinstance(vlans, list): - vlans = vlans.split(",") - return [int(_id) for _id in vlans] - - -@pytest.fixture(scope="module") -def vlan_index_number(vlans_list): - return get_vlan_index_number(vlans_list=vlans_list) - - @pytest.fixture(scope="session") def cluster_network_mtu(admin_client): network_resource = Network(name=CLUSTER, client=admin_client) diff --git a/tests/network/l2_bridge/conftest.py b/tests/network/l2_bridge/conftest.py index 7509ee428b..0fc9324da3 100644 --- a/tests/network/l2_bridge/conftest.py +++ b/tests/network/l2_bridge/conftest.py @@ -18,7 +18,6 @@ UNIQUE_CLIENT_ID, verify_dhcpd_activated, ) -from utilities.constants import LINUX_BRIDGE, WORKER_NODE_LABEL_KEY from utilities.data_utils import name_prefix from utilities.infra import get_node_selector_dict from utilities.network import ( @@ -288,34 +287,6 @@ def started_vmb_dhcp_client(l2_bridge_running_vm_b, eth3_nmcli_connection_uuid): ) -@pytest.fixture(scope="package") -def bridge_nncp( - nmstate_dependent_placeholder: None, - admin_client: DynamicClient, - hosts_common_available_ports: list[str], -) -> Generator[libnncp.NodeNetworkConfigurationPolicy]: - with libnncp.NodeNetworkConfigurationPolicy( - client=admin_client, - name="l2-bridge-test-nncp", - desired_state=libnncp.DesiredState( - interfaces=[ - libnncp.Interface( - name="br1-test", - type=LINUX_BRIDGE, - state=libnncp.Resource.Interface.State.UP, - bridge=libnncp.Bridge( - port=[libnncp.Port(name=hosts_common_available_ports[-1])], - options=libnncp.BridgeOptions(libnncp.STP(enabled=False)), - ), - ) - ] - ), - node_selector={WORKER_NODE_LABEL_KEY: ""}, - ) as nncp_br: - nncp_br.wait_for_status_success() - yield nncp_br - - @pytest.fixture(scope="class") def bridge_nad( admin_client: DynamicClient, diff --git a/tests/network/l2_bridge/nad_ref_change/conftest.py b/tests/network/l2_bridge/nad_ref_change/conftest.py index c226d46536..6f8fa3acfd 100644 --- a/tests/network/l2_bridge/nad_ref_change/conftest.py +++ b/tests/network/l2_bridge/nad_ref_change/conftest.py @@ -4,9 +4,8 @@ from kubernetes.dynamic import DynamicClient from ocp_resources.namespace import Namespace -from libs.net import nodenetworkconfigurationpolicy as libnncp from libs.net.ip import filter_link_local_addresses, random_cidr_addresses_by_family -from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition +from libs.net.netattachdef import NetworkAttachmentDefinition from libs.net.vmspec import lookup_iface_status, wait_for_ifaces_status from libs.vm.vm import BaseVirtualMachine from tests.network.l2_bridge.libl2bridge import LINUX_BRIDGE_IFACE_NAME_1, LINUX_BRIDGE_IFACE_NAME_2 @@ -19,44 +18,6 @@ from tests.network.libs.connectivity import ARP_ISOLATION_SYSCTL_CMD, poll_tcp_connectivity -@pytest.fixture(scope="module") -def bridge_nad_a( - admin_client: DynamicClient, - namespace: Namespace, - bridge_nncp: libnncp.NodeNetworkConfigurationPolicy, - vlan_index_number: Generator[int], -) -> Generator[NetworkAttachmentDefinition]: - bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore - with NetworkAttachmentDefinition( - name="nad-vlan-a", - namespace=namespace.name, - config=NetConfig( - name="nad-vlan-a", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))] - ), - client=admin_client, - ) as nad: - yield nad - - -@pytest.fixture(scope="module") -def bridge_nad_b( - admin_client: DynamicClient, - namespace: Namespace, - bridge_nncp: libnncp.NodeNetworkConfigurationPolicy, - vlan_index_number: Generator[int], -) -> Generator[NetworkAttachmentDefinition]: - bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore[union-attr, index] - with NetworkAttachmentDefinition( - name="nad-vlan-b", - namespace=namespace.name, - config=NetConfig( - name="nad-vlan-b", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))] - ), - client=admin_client, - ) as nad: - yield nad - - @pytest.fixture(scope="module") def ref_vm( namespace: Namespace, diff --git a/tests/network/libs/nad_ref.py b/tests/network/libs/nad_ref.py deleted file mode 100644 index c929e8eb1e..0000000000 --- a/tests/network/libs/nad_ref.py +++ /dev/null @@ -1,24 +0,0 @@ -from copy import deepcopy - -from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status -from libs.vm.vm import BaseVirtualMachine - - -def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None: - """Update secondary network NAD references and wait for the change to be fully applied. - - Patches the VM spec atomically, then waits for the MigrationRequired condition to - appear (change detected) and disappear (migration completed). - - Args: - vm: The virtual machine to update. - nad_name_by_net: Mapping of spec network name to new NAD name. - """ - resource_version = vm.vmi.instance.metadata.resourceVersion - networks = deepcopy(vm.template_spec.networks) or [] - for network in networks: - if network.name in nad_name_by_net and network.multus: - network.multus.networkName = nad_name_by_net[network.name] - vm.set_networks(networks=networks) - wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version) - wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired") diff --git a/tests/network/localnet/nad_ref_change/test_nad_ref_change.py b/tests/network/localnet/nad_ref_change/test_nad_ref_change.py index be4e4dde1d..5a36f902df 100644 --- a/tests/network/localnet/nad_ref_change/test_nad_ref_change.py +++ b/tests/network/localnet/nad_ref_change/test_nad_ref_change.py @@ -13,9 +13,8 @@ import pytest from libs.net.ip import filter_link_local_addresses -from libs.net.vmspec import lookup_iface_status +from libs.net.vmspec import lookup_iface_status, update_nad_references from tests.network.libs.connectivity import poll_tcp_connectivity -from tests.network.libs.nad_ref import update_nad_references from tests.network.localnet.liblocalnet import ( GUEST_1ST_IFACE_NAME, GUEST_2ND_IFACE_NAME, diff --git a/tests/network/utils.py b/tests/network/utils.py index c89937ac90..53dd0d4a9b 100644 --- a/tests/network/utils.py +++ b/tests/network/utils.py @@ -257,11 +257,6 @@ def assert_nncp_successfully_configured(nncp): raise -def get_vlan_index_number(vlans_list): - yield from vlans_list - raise ValueError(f"vlans list is exhausted. Current list size is {len(vlans_list)} and all vlans are in use.") - - def get_destination_ip_address(destination_vm): dst_ip = get_ip_from_vm_or_virt_handler_pod( family=IPV4_STR, diff --git a/tests/observability/metrics/conftest.py b/tests/observability/metrics/conftest.py index 6d567b6841..5c90eca4d2 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -1,6 +1,5 @@ import logging import shlex -from collections.abc import Generator import bitmath import pytest @@ -16,8 +15,6 @@ from pytest_testconfig import py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler -from libs.net import nodenetworkconfigurationpolicy as libnncp -from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition from libs.net.vmspec import update_nad_references from libs.vm.factory import base_vmspec, fedora_vm from libs.vm.spec import Devices, Interface, Multus, Network @@ -56,7 +53,6 @@ KUBEVIRT_VMI_MEMORY_SWAP_OUT_TRAFFIC_BYTES, KUBEVIRT_VMI_MEMORY_UNUSED_BYTES, KUBEVIRT_VMI_MEMORY_USABLE_BYTES, - LINUX_BRIDGE, MIGRATION_POLICY_VM_LABEL, ONE_CPU_CORE, ONE_CPU_THREAD, @@ -73,7 +69,6 @@ TWO_CPU_THREADS, U1_MEDIUM_STR, VIRT_TEMPLATE_VALIDATOR, - WORKER_NODE_LABEL_KEY, Images, ) from utilities.hco import ResourceEditorValidateHCOReconcile, enabled_aaq_in_hco @@ -674,81 +669,14 @@ def expected_cpu_affinity_metric_value(admin_client, vm_with_cpu_spec): return str(cpu_count_from_vm_node * cpu_count_from_vm) -_NAD_SWAP_BRIDGE_NAME = "brnadswap" _NAD_SWAP_SECONDARY_IFACE = "secondary" -@pytest.fixture(scope="class") -def nad_swap_bridge_nncp( - nmstate_dependent_placeholder, - admin_client, - hosts_common_available_ports, -) -> Generator[libnncp.NodeNetworkConfigurationPolicy]: - with libnncp.NodeNetworkConfigurationPolicy( - client=admin_client, - name="nad-swap-vnic-info-nncp", - desired_state=libnncp.DesiredState( - interfaces=[ - libnncp.Interface( - name=_NAD_SWAP_BRIDGE_NAME, - type=LINUX_BRIDGE, - state=libnncp.Resource.Interface.State.UP, - bridge=libnncp.Bridge( - port=[libnncp.Port(name=hosts_common_available_ports[-1])], - options=libnncp.BridgeOptions(libnncp.STP(enabled=False)), - ), - ) - ] - ), - node_selector={WORKER_NODE_LABEL_KEY: ""}, - ) as nncp: - nncp.wait_for_status_success() - yield nncp - - -@pytest.fixture(scope="class") -def nad_a_for_vnic_info( - admin_client, - namespace, - nad_swap_bridge_nncp, - next_vlan_index_number, -) -> Generator[NetworkAttachmentDefinition]: - with NetworkAttachmentDefinition( - name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-a", - namespace=namespace.name, - config=NetConfig( - name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-a", - plugins=[CNIPluginBridgeConfig(bridge=_NAD_SWAP_BRIDGE_NAME, vlan=next(next_vlan_index_number))], - ), - client=admin_client, - ) as nad: - yield nad - - -@pytest.fixture(scope="class") -def nad_b_for_vnic_info( - admin_client, - namespace, - nad_swap_bridge_nncp, - next_vlan_index_number, -): - with NetworkAttachmentDefinition( - name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b", - namespace=namespace.name, - config=NetConfig( - name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b", - plugins=[CNIPluginBridgeConfig(bridge=_NAD_SWAP_BRIDGE_NAME, vlan=next(next_vlan_index_number))], - ), - client=admin_client, - ) as nad: - yield nad - - @pytest.fixture(scope="class") def vm_for_nad_swap_test( unprivileged_client, namespace, - nad_a_for_vnic_info, + bridge_nad_a, ): vm_name = "vm-nad-swap-vnic-info" spec = base_vmspec() @@ -760,7 +688,7 @@ def vm_for_nad_swap_test( ) spec.template.spec.networks = [ Network(name="default", pod={}), - Network(name=_NAD_SWAP_SECONDARY_IFACE, multus=Multus(networkName=nad_a_for_vnic_info.name)), + Network(name=_NAD_SWAP_SECONDARY_IFACE, multus=Multus(networkName=bridge_nad_a.name)), ] with fedora_vm(namespace=namespace.name, name=vm_name, client=unprivileged_client, spec=spec) as vm: vm.start(wait=True) @@ -770,11 +698,11 @@ def vm_for_nad_swap_test( @pytest.fixture(scope="class") def post_nad_swap_vm( vm_for_nad_swap_test, - nad_b_for_vnic_info, + bridge_nad_b, ): update_nad_references( vm=vm_for_nad_swap_test, - nad_name_by_net={_NAD_SWAP_SECONDARY_IFACE: nad_b_for_vnic_info.name}, + nad_name_by_net={_NAD_SWAP_SECONDARY_IFACE: bridge_nad_b.name}, ) yield vm_for_nad_swap_test @@ -782,7 +710,7 @@ def post_nad_swap_vm( @pytest.fixture(scope="class") def expected_vnic_info_after_swap( post_nad_swap_vm, - nad_b_for_vnic_info, + bridge_nad_b, ): vm_interfaces = post_nad_swap_vm.instance.spec.template.spec.domain.devices.interfaces secondary_interface = next(iface for iface in vm_interfaces if iface["name"] == _NAD_SWAP_SECONDARY_IFACE) @@ -791,5 +719,5 @@ def expected_vnic_info_after_swap( "vnic_name": _NAD_SWAP_SECONDARY_IFACE, BINDING_NAME: binding_info[BINDING_NAME], BINDING_TYPE: binding_info[BINDING_TYPE], - "network": nad_b_for_vnic_info.name, + "network": bridge_nad_b.name, } diff --git a/tests/utils.py b/tests/utils.py index 874dcc7633..d830100a19 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -782,3 +782,8 @@ def create_windows2022_vm_with_vtpm_from_registry( running_vm(vm=vm) wait_for_windows_vm(vm=vm, version="2022") yield vm + + +def get_vlan_index_number(vlans_list: list[int]) -> Generator[int]: + yield from vlans_list + raise ValueError(f"vlans list is exhausted. Current list size is {len(vlans_list)} and all vlans are in use.")