Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion tests/network/l2_bridge/libl2bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from libs.net.ip import random_ipv4_address
from libs.net.vmspec import lookup_iface_status, lookup_iface_status_ip, wait_for_missing_iface_status
from libs.vm.factory import base_vmspec, fedora_vm
from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Multus, Network
from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Metadata, Multus, Network
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
Anatw marked this conversation as resolved.
Comment thread
Anatw marked this conversation as resolved.
Comment thread
Anatw marked this conversation as resolved.
from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage
from tests.network.libs import cloudinit
from tests.network.libs.cloudinit import primary_iface_cloud_init
Expand Down Expand Up @@ -380,6 +380,7 @@ def secondary_network_vm(
secondary_iface_name: str,
secondary_iface_addresses: list[str],
affinity: Affinity | None = None,
labels: dict[str, str] | None = None,
) -> BaseVirtualMachine:
"""Create a Fedora VM with a masquerade primary interface and a secondary Linux bridge interface.

Expand All @@ -391,6 +392,7 @@ def secondary_network_vm(
secondary_iface_name: Name of the secondary network interface in the VM spec.
secondary_iface_addresses: CIDR addresses to assign to the secondary interface via cloud-init.
affinity: Optional node or pod affinity rules for scheduling.
labels: Optional labels to apply to the VM template metadata for pod scheduling.
"""
spec = base_vmspec()
spec.template.spec.domain.devices.interfaces = [ # type: ignore
Expand All @@ -404,6 +406,11 @@ def secondary_network_vm(
if affinity:
spec.template.spec.affinity = affinity

if labels:
spec.template.metadata = spec.template.metadata or Metadata()
spec.template.metadata.labels = spec.template.metadata.labels or {}
spec.template.metadata.labels.update(labels)

ethernets = {}
primary = primary_iface_cloud_init()
if primary:
Expand Down
110 changes: 110 additions & 0 deletions tests/network/l2_bridge/migration_stuntime/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from collections.abc import Generator
from typing import Final

import pytest
from kubernetes.dynamic import DynamicClient
from ocp_resources.namespace import Namespace

import tests.network.libs.nodenetworkconfigurationpolicy as libnncp
from libs.net import netattachdef as libnad
from libs.net.ip import random_ipv4_address, random_ipv6_address
from libs.net.vmspec import lookup_iface_status_ip
from libs.vm.affinity import new_pod_affinity
from libs.vm.vm import BaseVirtualMachine
from tests.network.l2_bridge.libl2bridge import secondary_network_vm
from tests.network.libs.stuntime import SERVER_VM_LABEL, ContinuousPing

STUNTIME_BRIDGE_IFACE_NAME: Final[str] = "stuntime-bridge"


@pytest.fixture(scope="module")
def l2_bridge_stuntime_nad(
admin_client: DynamicClient,
namespace: Namespace,
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
) -> Generator[libnad.NetworkAttachmentDefinition]:
nad_name = "l2-bridge-nad"
config = libnad.NetConfig(
name=nad_name,
Comment thread
Anatw marked this conversation as resolved.
plugins=[libnad.CNIPluginBridgeConfig(bridge=bridge_nncp.desired_state_spec.interfaces[0].name)], # type: ignore[index]
)
with libnad.NetworkAttachmentDefinition(
name=nad_name,
namespace=namespace.name,
config=config,
client=admin_client,
) as nad:
yield nad


@pytest.fixture(scope="class")
def stuntime_server_vm(
unprivileged_client: DynamicClient,
namespace: Namespace,
l2_bridge_ip_family: int,
l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition,
) -> Generator[BaseVirtualMachine]:
with secondary_network_vm(
namespace=namespace.name,
name="l2bridge-stuntime-server",
client=unprivileged_client,
nad_name=l2_bridge_stuntime_nad.name,
secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME,
secondary_iface_addresses=[
f"{random_ipv4_address(net_seed=0, host_address=1)}/24",
f"{random_ipv6_address(net_seed=0, host_address=1)}/64",
],
labels=dict([SERVER_VM_LABEL]),
) as server_vm:
server_vm.start(wait=True)
server_vm.wait_for_agent_connected()
yield server_vm


@pytest.fixture(scope="class")
def stuntime_client_vm(
unprivileged_client: DynamicClient,
namespace: Namespace,
l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition,
stuntime_server_vm: BaseVirtualMachine,
) -> Generator[BaseVirtualMachine]:
with secondary_network_vm(
namespace=namespace.name,
name="l2bridge-stuntime-client",
client=unprivileged_client,
nad_name=l2_bridge_stuntime_nad.name,
secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME,
secondary_iface_addresses=[
f"{random_ipv4_address(net_seed=0, host_address=2)}/24",
f"{random_ipv6_address(net_seed=0, host_address=2)}/64",
],
affinity=new_pod_affinity(label=SERVER_VM_LABEL),
) as client_vm:
client_vm.start(wait=True)
client_vm.wait_for_agent_connected()
yield client_vm


@pytest.fixture(scope="class")
def l2_bridge_ip_family(request: pytest.FixtureRequest) -> int:
"""IP family for stuntime measurement, activated by per-test parametrize."""
return request.param


@pytest.fixture()
def l2_bridge_active_ping(
l2_bridge_ip_family: int,
stuntime_server_vm: BaseVirtualMachine,
stuntime_client_vm: BaseVirtualMachine,
) -> Generator[ContinuousPing]:
"""Continuous ping session from client to server for stuntime measurement."""
server_ip = str(
lookup_iface_status_ip(
vm=stuntime_server_vm,
iface_name=STUNTIME_BRIDGE_IFACE_NAME,
ip_family=l2_bridge_ip_family,
)
)

with ContinuousPing(source_vm=stuntime_client_vm, destination_ip=server_ip) as ping:
yield ping
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@
Stuntime is defined as the connectivity gap from last successful reply before loss
to first successful reply after recovery.

Stuntime is measured using ICMP ping from client to server in 0.1s intervals, using ping -D so each
log line includes a timestamp for gap calculation.
The under-test VMs are configured on a Linux bridge secondary network, with a single interface,
Stuntime is measured using ICMP ping from client to server in 0.1s intervals.
The under-test VMs are configured with a secondary Linux bridge interface,
on which IPv4/IPv6 static addresses will be defined according to the environment the test runs on.

Client - The connectivity initiator VM that runs continuous ping toward the server VM.
Server - The connectivity listener VM that receives the ping and responds.

STP Reference:
Comment thread
Anatw marked this conversation as resolved.
https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
"""

import pytest

__test__ = False
from libs.vm.affinity import new_pod_anti_affinity
from tests.network.libs.stuntime import SERVER_VM_LABEL, STUNTIME_THRESHOLD_SECONDS, measure_stuntime
from utilities.virt import migrate_vm_and_verify

pytestmark = [pytest.mark.tier3]

"""
Parametrize:
- ip_family:
- l2_bridge_ip_family:
- ipv4 [Markers: ipv4]
- ipv6 [Markers: ipv6]

Expand All @@ -36,9 +38,19 @@


@pytest.mark.incremental
@pytest.mark.parametrize(
"l2_bridge_ip_family",
[
pytest.param(4, marks=pytest.mark.ipv4, id="ipv4"),
pytest.param(6, marks=pytest.mark.ipv6, id="ipv6"),
],
indirect=True,
)
class TestMigrationStuntime:
@pytest.mark.polarion("CNV-15252")
def test_client_migrates_off_server_node(self):
def test_client_migrates_off_server_node(
self, admin_client, l2_bridge_ip_family, stuntime_client_vm, l2_bridge_active_ping
):
"""
Test that measured stuntime does not exceed the global threshold when the client
VM migrates from the node hosting the server VM into a different node.
Expand All @@ -58,6 +70,12 @@ def test_client_migrates_off_server_node(self):
Expected:
- Measured stuntime does not exceed the global threshold.
"""
stuntime_client_vm.set_template_affinity(affinity=new_pod_anti_affinity(label=SERVER_VM_LABEL))
migrate_vm_and_verify(vm=stuntime_client_vm, client=admin_client)
measured_stuntime = measure_stuntime(active_ping=l2_bridge_active_ping)
assert measured_stuntime <= STUNTIME_THRESHOLD_SECONDS, (
f"Stuntime {measured_stuntime}s exceeds threshold ({STUNTIME_THRESHOLD_SECONDS}s)"
)

@pytest.mark.polarion("CNV-15253")
def test_client_migrates_between_non_server_nodes(self):
Expand Down Expand Up @@ -168,3 +186,9 @@ def test_server_migrates_to_client_node(self):
Expected:
- Measured stuntime does not exceed the global threshold.
"""

test_client_migrates_between_non_server_nodes.__test__ = False
test_client_migrates_to_server_node.__test__ = False
test_server_migrates_off_client_node.__test__ = False
test_server_migrates_between_non_client_nodes.__test__ = False
test_server_migrates_to_client_node.__test__ = False
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Helpers for OVN localnet migration stuntime tests."""
"""Helpers for migration stuntime tests."""
Comment thread
Anatw marked this conversation as resolved.

import ipaddress
import logging
Expand All @@ -23,35 +23,6 @@ class InsufficientStuntimeDataError(ValueError):
"""Raised when ping log has too few successful replies to compute stuntime."""


def measure_stuntime(active_ping: "ContinuousPing") -> float:
"""Stop the continuous ping session and compute the measured stuntime.

Args:
active_ping: Active continuous ping session to stop and evaluate.

Returns:
Measured stuntime in seconds.
"""
active_ping.stop()
_, _, lost = active_ping.report()
return compute_stuntime(lost_packets=lost)


def compute_stuntime(lost_packets: int) -> float:
"""Compute stuntime from lost packet count.

Args:
lost_packets: Number of packets lost during migration.

Returns:
Stuntime in seconds (connectivity gap).
"""
# Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery
stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS
LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)")
return stuntime


class ContinuousPing:
"""Context manager for continuous ping monitoring during VM operations.

Expand Down Expand Up @@ -141,3 +112,32 @@ def _verify_ping_reaches_destination(self) -> None:
],
timeout=DEFAULT_COMMAND_TIMEOUT_SECONDS + 5,
)


def measure_stuntime(active_ping: ContinuousPing) -> float:
"""Stop a continuous ping session and compute the stuntime.

Args:
active_ping: Active ContinuousPing session to stop and evaluate.

Returns:
Measured stuntime in seconds.
"""
active_ping.stop()
_, _, lost = active_ping.report()
return _compute_stuntime(lost_packets=lost)


def _compute_stuntime(lost_packets: int) -> float:
Comment thread
Anatw marked this conversation as resolved.
"""Compute stuntime from lost packet count.

Args:
lost_packets: Number of packets lost during migration.

Returns:
Stuntime in seconds (connectivity gap).
"""
# Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery
stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS
LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)")
return stuntime
2 changes: 1 addition & 1 deletion tests/network/localnet/migration_stuntime/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
from libs.vm.vm import BaseVirtualMachine
from tests.network.libs import cloudinit
from tests.network.libs import cluster_user_defined_network as libcudn
from tests.network.libs.stuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing
from tests.network.localnet.liblocalnet import (
GUEST_1ST_IFACE_NAME,
LOCALNET_OVS_BRIDGE_INTERFACE,
ip_addresses_from_pool,
libnncp,
localnet_vm,
)
from tests.network.localnet.migration_stuntime.libstuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing

LOGGER = logging.getLogger(__name__)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
Client - The connectivity initiator VM that runs continuous ping toward the server VM.
Server - The connectivity listener VM that receives the ping and responds.

STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main
/stps/sig-network/stuntime_measurement.md
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
"""

import pytest

from libs.vm.affinity import new_pod_affinity, new_pod_anti_affinity
from tests.network.localnet.migration_stuntime.libstuntime import (
Comment thread
Anatw marked this conversation as resolved.
from tests.network.libs.stuntime import (
CLIENT_VM_LABEL,
SERVER_VM_LABEL,
STUNTIME_THRESHOLD_SECONDS,
Expand Down