Skip to content
File renamed without changes.
21 changes: 21 additions & 0 deletions libs/net/cluster.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ipaddress
import logging
from collections.abc import Generator
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
from functools import cache

from pytest_testconfig import py_config
Expand Down Expand Up @@ -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]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cluster_vlan_iterator is a one-liner yield from wrapper whose only addition is a ValueError after exhaustion which breaks the iterator protocol. StopIteration is raised automatically when a generator returns; the traceback already points to the exact next() call site, so the custom message adds nothing. The fixture could just do return iter(cluster_vlans()) and get correct behavior with no wrapper function needed.

Simply put, from my perspective _cluster_vlans() should be renamed to cluster_vlans() or just vlan_ids() (because it resides under cluster module already) and then the fixture should be (without next_ prefix: callers do next(next_...) — redundant and confusing):

@pytest.fixture(scope="module")
def cluster_vlan_ids():
    return iter(vlan_ids())

@azhivovk

@azhivovk azhivovk Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clearer than the current implementation
Done, thanks

Now we can use this fixture like this next(cluster_vlan_ids)

"""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.")
Comment thread
OhadRevah marked this conversation as resolved.
Outdated


@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]
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Execution Plan

Run smoke tests: False
Verified: smoke-marked files are in tests/storage/, tests/virt/, tests/install_upgrade_operators/, and tests/infrastructure/. None of those files or their conftest hierarchy import cluster_vlan_iterator, next_vlan_index_number, wait_for_vmi_condition_status, wait_for_no_vmi_condition, or update_nad_references. No traceable dependency path.

Run gating tests: False
Verified: gating-marked network test files (tests/network/nmstate/, tests/network/connectivity/, tests/network/localnet/test_default_bridge.py, etc.) have zero imports of any modified symbol. update_nad_references callers (tests/network/l2_bridge/nad_ref_change/, tests/network/localnet/nad_ref_change/) carry no gating marker.

Affected tests to run

New test (primary deliverable of this PR):

  • tests/observability/metrics/test_vms_metrics.py::TestVmVnicInfo::test_metric_kubevirt_vm_vnic_info_after_nad_swap

Import-path-only refactors (libnncp: tests.network.libs.* -> libs.net.*) — collection verification only:

  • pytest --collect-only tests/network/bgp/
  • pytest --collect-only tests/network/l2_bridge/
  • pytest --collect-only tests/network/localnet/

Real tests (cluster required)

tests/conftest.py promotes bridge_nncp (package-scope), bridge_nad_a, bridge_nad_b, vlan_index_number, and vlans_list (session-scope) from per-directory conftest files to the root conftest. These run at session/setup time and must be verified on a real cluster:

Happy path (new observability test end-to-end):
pytest tests/observability/metrics/test_vms_metrics.py::TestVmVnicInfo::test_metric_kubevirt_vm_vnic_info_after_nad_swap -s
Expected: VM starts attached to bridge_nad_a; NAD swap triggers live migration; kubevirt_vm_vnic_info / kubevirt_vmi_vnic_info labels update to reflect bridge_nad_b.

Regression (existing NAD-ref-change tests still collect after fixture relocation):
pytest --collect-only tests/network/l2_bridge/nad_ref_change/ tests/network/localnet/nad_ref_change/
Expected: session collects without import errors; vlan_index_number resolves from tests/conftest.py (moved from tests/network/conftest.py).

WAIT_FOR_STATUS_TIMEOUT_SEC = 120
WAIT_FOR_STATUS_INTERVAL_SEC = 5
Expand Down
4 changes: 2 additions & 2 deletions libs/net/vmspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
return cluster_vlan_iterator()
2 changes: 1 addition & 1 deletion tests/network/bgp/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
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
from libs.net.udn import UDN_BINDING_DEFAULT_PLUGIN_NAME, create_udn_namespace
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,
Expand Down
2 changes: 1 addition & 1 deletion tests/network/l2_bridge/bandwidth/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/network/l2_bridge/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/network/l2_bridge/migration_stuntime/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/network/l2_bridge/nad_ref_change/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/network/libs/cloudinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion tests/network/libs/cluster_user_defined_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/network/localnet/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/network/localnet/ipam/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/network/localnet/liblocalnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

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
from libs.vm.spec import Affinity, CloudInitNoCloud, Devices, Interface, Metadata, Network
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

Expand Down
133 changes: 132 additions & 1 deletion tests/observability/metrics/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
Comment thread
OhadRevah marked this conversation as resolved.
Comment thread
OhadRevah marked this conversation as resolved.
import shlex
from collections.abc import Generator

import bitmath
import pytest
Expand All @@ -15,7 +16,13 @@
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.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,
Expand All @@ -27,6 +34,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,
Expand All @@ -37,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,
Expand All @@ -47,6 +55,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,
Expand All @@ -63,6 +72,7 @@
TWO_CPU_THREADS,
U1_MEDIUM_STR,
VIRT_TEMPLATE_VALIDATOR,
WORKER_NODE_LABEL_KEY,
Images,
)
from utilities.hco import ResourceEditorValidateHCOReconcile, enabled_aaq_in_hco
Expand Down Expand Up @@ -661,3 +671,124 @@ 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(
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
nmstate_dependent_placeholder,
admin_client,
hosts_common_available_ports,
) -> Generator[libnncp.NodeNetworkConfigurationPolicy]:
with libnncp.NodeNetworkConfigurationPolicy(
client=admin_client,
name="nad-swap-vnic-info-nncp",
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
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(
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
admin_client,
namespace,
nad_swap_bridge_nncp,
next_vlan_index_number,
) -> Generator[NetworkAttachmentDefinition]:
with NetworkAttachmentDefinition(
name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-a",
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
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))],
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
),
client=admin_client,
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
) as nad:
yield nad


@pytest.fixture(scope="class")
def nad_b_for_vnic_info(
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
admin_client,
namespace,
nad_swap_bridge_nncp,
next_vlan_index_number,
):
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
with NetworkAttachmentDefinition(
name=f"{_NAD_SWAP_BRIDGE_NAME}-nad-b",
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
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,
Comment thread
OhadRevah marked this conversation as resolved.
Outdated
) as nad:
yield nad


@pytest.fixture(scope="class")
def vm_for_nad_swap_test(
unprivileged_client,
namespace,
nad_a_for_vnic_info,
):
Comment thread
OhadRevah marked this conversation as resolved.
vm_name = "vm-nad-swap-vnic-info"
spec = base_vmspec()
Comment thread
OhadRevah marked this conversation as resolved.
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


@pytest.fixture(scope="class")
def post_nad_swap_vm(
vm_for_nad_swap_test,
nad_b_for_vnic_info,
):
update_nad_references(
vm=vm_for_nad_swap_test,
nad_name_by_net={_NAD_SWAP_SECONDARY_IFACE: nad_b_for_vnic_info.name},
)
yield vm_for_nad_swap_test


@pytest.fixture(scope="class")
def expected_vnic_info_after_swap(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is a fixture and not just helper? This is just a helper function in reality. Called directly in the test after the swap. WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what happen here in the test, this fixture depends on post_nad_swap_vm and nad_b_for_vnic_info, so it clearer in the code, then I just use the dict for checking the values, I don't think we need an helper for it.

post_nad_swap_vm,
nad_b_for_vnic_info,
):
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,
}
Loading