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/libs/net/vmspec.py b/libs/net/vmspec.py index adff3acf3d..5c1dea768e 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 @@ -206,7 +207,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 +250,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, @@ -299,3 +300,35 @@ 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 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. + """ + 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 = 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: + 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 a15cdf1155..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 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 @@ -2739,3 +2743,84 @@ def hugepages_gib_values(workers): for worker in workers if (value := worker.instance.status.allocatable.get(NODE_HUGE_PAGES_1GI_KEY)) ] + + +@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 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/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/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/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..0fc9324da3 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 @@ -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/migration_stuntime/conftest.py b/tests/network/l2_bridge/migration_stuntime/conftest.py index cce99232c2..926b8cea47 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..6f8fa3acfd 100644 --- a/tests/network/l2_bridge/nad_ref_change/conftest.py +++ b/tests/network/l2_bridge/nad_ref_change/conftest.py @@ -5,7 +5,7 @@ from ocp_resources.namespace import Namespace 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 @@ -15,48 +15,9 @@ 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 -@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/l2_bridge/nad_ref_change/test_nad_ref_change.py b/tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py index ea415b374c..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,7 +14,7 @@ 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, @@ -22,7 +22,6 @@ assert_connectivity, assert_no_connectivity, ) -from tests.network.libs.nad_ref import update_nad_references @pytest.mark.usefixtures("baseline_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/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/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 558236eac0..525ee710e1 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 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 4330d744dd..5c90eca4d2 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -15,7 +15,12 @@ from pytest_testconfig import py_config from timeout_sampler import TimeoutExpiredError, TimeoutSampler +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 ( + BINDING_NAME, + BINDING_TYPE, GUEST_LOAD_TIME_PERIODS, KUBEVIRT_CONSOLE_ACTIVE_CONNECTIONS_BY_VMI, KUBEVIRT_VM_CREATED_BY_POD_TOTAL, @@ -27,6 +32,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, @@ -661,3 +667,57 @@ 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_SECONDARY_IFACE = "secondary" + + +@pytest.fixture(scope="class") +def vm_for_nad_swap_test( + unprivileged_client, + namespace, + bridge_nad_a, +): + vm_name = "vm-nad-swap-vnic-info" + 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=bridge_nad_a.name)), + ] + with fedora_vm(namespace=namespace.name, name=vm_name, client=unprivileged_client, spec=spec) as vm: + vm.start(wait=True) + yield vm + + +@pytest.fixture(scope="class") +def post_nad_swap_vm( + vm_for_nad_swap_test, + bridge_nad_b, +): + update_nad_references( + vm=vm_for_nad_swap_test, + nad_name_by_net={_NAD_SWAP_SECONDARY_IFACE: bridge_nad_b.name}, + ) + yield vm_for_nad_swap_test + + +@pytest.fixture(scope="class") +def expected_vnic_info_after_swap( + post_nad_swap_vm, + 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) + 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": bridge_nad_b.name, + } diff --git a/tests/observability/metrics/test_vms_metrics.py b/tests/observability/metrics/test_vms_metrics.py index 36d317cc34..0f12f03052 100644 --- a/tests/observability/metrics/test_vms_metrics.py +++ b/tests/observability/metrics/test_vms_metrics.py @@ -461,7 +461,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. @@ -474,13 +476,17 @@ def test_metric_kubevirt_vm_vnic_info_after_nad_swap(self, query): Steps: 1. Swap the VM secondary network reference from NAD-A to NAD-B - 2. Query vnic_info metric for the secondary interface + 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 """ - - 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: diff --git a/tests/observability/metrics/utils.py b/tests/observability/metrics/utils.py index ee1931a746..2c1f49f95e 100644 --- a/tests/observability/metrics/utils.py +++ b/tests/observability/metrics/utils.py @@ -595,21 +595,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 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.")