Skip to content

Commit c64efa3

Browse files
authored
net, tests, stuntime: Add L2 bridge client migration stuntime scenario (#4867)
##### Short description: Implement stuntime scenario over Linux bridge in which the client VM migrates off the server node. ##### More details: ##### What this PR does / why we need it: Establish a performance baseline and enable regression detection for Linux bridge live migration connectivity gaps (stuntime), mirroring the existing OVN localnet coverage. ##### Which issue(s) this PR fixes: ##### Special notes for reviewer: - The first commit is dedicated to the extraction of the shared stuntime lib, and the consequent import changes needed in the localnet stuntime sub-package. - For now, the global stuntime threshold is set to a 5s placeholder. Once we finish automating the remaining scenarios and have the baseline data to calibrate our expectations, we’ll replace this with a more precise, data-driven value. - measure_stuntime() is also introduced in PR #4833. Whichever merges first takes precedence — the other will rebase and remove the duplicate. ##### jira-ticket: https://redhat.atlassian.net/browse/CNV-84398 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added end-to-end L2 bridge migration tests with reusable server/client VM fixtures, IPv4/IPv6 parametrization, and an active ping-based stuntime measurement; updated a migration test to use the new fixtures and disabled several variants pending validation. * **Chores** * Consolidated stuntime measurement into a shared helper used across test suites. * VM provisioning now supports optional template metadata labels and related import cleanup. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/RedHatQE/openshift-virtualization-tests/pull/4867?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 871a310 + 36eae7a commit c64efa3

6 files changed

Lines changed: 183 additions & 43 deletions

File tree

tests/network/l2_bridge/libl2bridge.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from libs.net.ip import random_ipv4_address
1313
from libs.net.vmspec import lookup_iface_status, lookup_iface_status_ip, wait_for_missing_iface_status
1414
from libs.vm.factory import base_vmspec, fedora_vm
15-
from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Multus, Network
15+
from libs.vm.spec import Affinity, CloudInitNoCloud, Interface, Metadata, Multus, Network
1616
from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage
1717
from tests.network.libs import cloudinit
1818
from tests.network.libs.cloudinit import primary_iface_cloud_init
@@ -383,6 +383,7 @@ def secondary_network_vm(
383383
secondary_iface_name: str,
384384
secondary_iface_addresses: list[str],
385385
affinity: Affinity | None = None,
386+
labels: dict[str, str] | None = None,
386387
) -> BaseVirtualMachine:
387388
"""Create a Fedora VM with a masquerade primary interface and a secondary Linux bridge interface.
388389
@@ -394,6 +395,7 @@ def secondary_network_vm(
394395
secondary_iface_name: Name of the secondary network interface in the VM spec.
395396
secondary_iface_addresses: CIDR addresses to assign to the secondary interface via cloud-init.
396397
affinity: Optional node or pod affinity rules for scheduling.
398+
labels: Optional labels to apply to the VM template metadata for pod scheduling.
397399
"""
398400
spec = base_vmspec()
399401
spec.template.spec.domain.devices.interfaces = [ # type: ignore
@@ -407,6 +409,11 @@ def secondary_network_vm(
407409
if affinity:
408410
spec.template.spec.affinity = affinity
409411

412+
if labels:
413+
spec.template.metadata = spec.template.metadata or Metadata()
414+
spec.template.metadata.labels = spec.template.metadata.labels or {}
415+
spec.template.metadata.labels.update(labels)
416+
410417
ethernets = {}
411418
primary = primary_iface_cloud_init()
412419
if primary:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from collections.abc import Generator
2+
from typing import Final
3+
4+
import pytest
5+
from kubernetes.dynamic import DynamicClient
6+
from ocp_resources.namespace import Namespace
7+
8+
import tests.network.libs.nodenetworkconfigurationpolicy as libnncp
9+
from libs.net import netattachdef as libnad
10+
from libs.net.ip import random_ipv4_address, random_ipv6_address
11+
from libs.net.vmspec import lookup_iface_status_ip
12+
from libs.vm.affinity import new_pod_affinity
13+
from libs.vm.vm import BaseVirtualMachine
14+
from tests.network.l2_bridge.libl2bridge import secondary_network_vm
15+
from tests.network.libs.stuntime import SERVER_VM_LABEL, ContinuousPing
16+
17+
STUNTIME_BRIDGE_IFACE_NAME: Final[str] = "stuntime-bridge"
18+
19+
20+
@pytest.fixture(scope="module")
21+
def l2_bridge_stuntime_nad(
22+
admin_client: DynamicClient,
23+
namespace: Namespace,
24+
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
25+
) -> Generator[libnad.NetworkAttachmentDefinition]:
26+
nad_name = "l2-bridge-nad"
27+
config = libnad.NetConfig(
28+
name=nad_name,
29+
plugins=[libnad.CNIPluginBridgeConfig(bridge=bridge_nncp.desired_state_spec.interfaces[0].name)], # type: ignore[index]
30+
)
31+
with libnad.NetworkAttachmentDefinition(
32+
name=nad_name,
33+
namespace=namespace.name,
34+
config=config,
35+
client=admin_client,
36+
) as nad:
37+
yield nad
38+
39+
40+
@pytest.fixture(scope="class")
41+
def stuntime_server_vm(
42+
unprivileged_client: DynamicClient,
43+
namespace: Namespace,
44+
l2_bridge_ip_family: int,
45+
l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition,
46+
) -> Generator[BaseVirtualMachine]:
47+
with secondary_network_vm(
48+
namespace=namespace.name,
49+
name="l2bridge-stuntime-server",
50+
client=unprivileged_client,
51+
nad_name=l2_bridge_stuntime_nad.name,
52+
secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME,
53+
secondary_iface_addresses=[
54+
f"{random_ipv4_address(net_seed=0, host_address=1)}/24",
55+
f"{random_ipv6_address(net_seed=0, host_address=1)}/64",
56+
],
57+
labels=dict([SERVER_VM_LABEL]),
58+
) as server_vm:
59+
server_vm.start(wait=True)
60+
server_vm.wait_for_agent_connected()
61+
yield server_vm
62+
63+
64+
@pytest.fixture(scope="class")
65+
def stuntime_client_vm(
66+
unprivileged_client: DynamicClient,
67+
namespace: Namespace,
68+
l2_bridge_stuntime_nad: libnad.NetworkAttachmentDefinition,
69+
stuntime_server_vm: BaseVirtualMachine,
70+
) -> Generator[BaseVirtualMachine]:
71+
with secondary_network_vm(
72+
namespace=namespace.name,
73+
name="l2bridge-stuntime-client",
74+
client=unprivileged_client,
75+
nad_name=l2_bridge_stuntime_nad.name,
76+
secondary_iface_name=STUNTIME_BRIDGE_IFACE_NAME,
77+
secondary_iface_addresses=[
78+
f"{random_ipv4_address(net_seed=0, host_address=2)}/24",
79+
f"{random_ipv6_address(net_seed=0, host_address=2)}/64",
80+
],
81+
affinity=new_pod_affinity(label=SERVER_VM_LABEL),
82+
) as client_vm:
83+
client_vm.start(wait=True)
84+
client_vm.wait_for_agent_connected()
85+
yield client_vm
86+
87+
88+
@pytest.fixture(scope="class")
89+
def l2_bridge_ip_family(request: pytest.FixtureRequest) -> int:
90+
"""IP family for stuntime measurement, activated by per-test parametrize."""
91+
return request.param
92+
93+
94+
@pytest.fixture()
95+
def l2_bridge_active_ping(
96+
l2_bridge_ip_family: int,
97+
stuntime_server_vm: BaseVirtualMachine,
98+
stuntime_client_vm: BaseVirtualMachine,
99+
) -> Generator[ContinuousPing]:
100+
"""Continuous ping session from client to server for stuntime measurement."""
101+
server_ip = str(
102+
lookup_iface_status_ip(
103+
vm=stuntime_server_vm,
104+
iface_name=STUNTIME_BRIDGE_IFACE_NAME,
105+
ip_family=l2_bridge_ip_family,
106+
)
107+
)
108+
109+
with ContinuousPing(source_vm=stuntime_client_vm, destination_ip=server_ip) as ping:
110+
yield ping

tests/network/l2_bridge/migration_stuntime/test_migration_stuntime.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,27 @@
66
Stuntime is defined as the connectivity gap from last successful reply before loss
77
to first successful reply after recovery.
88
9-
Stuntime is measured using ICMP ping from client to server in 0.1s intervals, using ping -D so each
10-
log line includes a timestamp for gap calculation.
11-
The under-test VMs are configured on a Linux bridge secondary network, with a single interface,
9+
Stuntime is measured using ICMP ping from client to server in 0.1s intervals.
10+
The under-test VMs are configured with a secondary Linux bridge interface,
1211
on which IPv4/IPv6 static addresses will be defined according to the environment the test runs on.
1312
1413
Client - The connectivity initiator VM that runs continuous ping toward the server VM.
1514
Server - The connectivity listener VM that receives the ping and responds.
1615
17-
STP Reference:
18-
https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
16+
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
1917
"""
2018

2119
import pytest
2220

23-
__test__ = False
21+
from libs.vm.affinity import new_pod_anti_affinity
22+
from tests.network.libs.stuntime import SERVER_VM_LABEL, STUNTIME_THRESHOLD_SECONDS, measure_stuntime
23+
from utilities.virt import migrate_vm_and_verify
24+
25+
pytestmark = [pytest.mark.tier3]
2426

2527
"""
2628
Parametrize:
27-
- ip_family:
29+
- l2_bridge_ip_family:
2830
- ipv4 [Markers: ipv4]
2931
- ipv6 [Markers: ipv6]
3032
@@ -36,9 +38,19 @@
3638

3739

3840
@pytest.mark.incremental
41+
@pytest.mark.parametrize(
42+
"l2_bridge_ip_family",
43+
[
44+
pytest.param(4, marks=pytest.mark.ipv4, id="ipv4"),
45+
pytest.param(6, marks=pytest.mark.ipv6, id="ipv6"),
46+
],
47+
indirect=True,
48+
)
3949
class TestMigrationStuntime:
4050
@pytest.mark.polarion("CNV-15252")
41-
def test_client_migrates_off_server_node(self):
51+
def test_client_migrates_off_server_node(
52+
self, admin_client, l2_bridge_ip_family, stuntime_client_vm, l2_bridge_active_ping
53+
):
4254
"""
4355
Test that measured stuntime does not exceed the global threshold when the client
4456
VM migrates from the node hosting the server VM into a different node.
@@ -58,6 +70,12 @@ def test_client_migrates_off_server_node(self):
5870
Expected:
5971
- Measured stuntime does not exceed the global threshold.
6072
"""
73+
stuntime_client_vm.set_template_affinity(affinity=new_pod_anti_affinity(label=SERVER_VM_LABEL))
74+
migrate_vm_and_verify(vm=stuntime_client_vm, client=admin_client)
75+
measured_stuntime = measure_stuntime(active_ping=l2_bridge_active_ping)
76+
assert measured_stuntime <= STUNTIME_THRESHOLD_SECONDS, (
77+
f"Stuntime {measured_stuntime}s exceeds threshold ({STUNTIME_THRESHOLD_SECONDS}s)"
78+
)
6179

6280
@pytest.mark.polarion("CNV-15253")
6381
def test_client_migrates_between_non_server_nodes(self):
@@ -168,3 +186,9 @@ def test_server_migrates_to_client_node(self):
168186
Expected:
169187
- Measured stuntime does not exceed the global threshold.
170188
"""
189+
190+
test_client_migrates_between_non_server_nodes.__test__ = False
191+
test_client_migrates_to_server_node.__test__ = False
192+
test_server_migrates_off_client_node.__test__ = False
193+
test_server_migrates_between_non_client_nodes.__test__ = False
194+
test_server_migrates_to_client_node.__test__ = False

tests/network/localnet/migration_stuntime/libstuntime.py renamed to tests/network/libs/stuntime.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Helpers for OVN localnet migration stuntime tests."""
1+
"""Helpers for migration stuntime tests."""
22

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

2525

26-
def measure_stuntime(active_ping: "ContinuousPing") -> float:
27-
"""Stop the continuous ping session and compute the measured stuntime.
28-
29-
Args:
30-
active_ping: Active continuous ping session to stop and evaluate.
31-
32-
Returns:
33-
Measured stuntime in seconds.
34-
"""
35-
active_ping.stop()
36-
_, _, lost = active_ping.report()
37-
return compute_stuntime(lost_packets=lost)
38-
39-
40-
def compute_stuntime(lost_packets: int) -> float:
41-
"""Compute stuntime from lost packet count.
42-
43-
Args:
44-
lost_packets: Number of packets lost during migration.
45-
46-
Returns:
47-
Stuntime in seconds (connectivity gap).
48-
"""
49-
# Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery
50-
stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS
51-
LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)")
52-
return stuntime
53-
54-
5526
class ContinuousPing:
5627
"""Context manager for continuous ping monitoring during VM operations.
5728
@@ -141,3 +112,32 @@ def _verify_ping_reaches_destination(self) -> None:
141112
],
142113
timeout=DEFAULT_COMMAND_TIMEOUT_SECONDS + 5,
143114
)
115+
116+
117+
def measure_stuntime(active_ping: ContinuousPing) -> float:
118+
"""Stop a continuous ping session and compute the stuntime.
119+
120+
Args:
121+
active_ping: Active ContinuousPing session to stop and evaluate.
122+
123+
Returns:
124+
Measured stuntime in seconds.
125+
"""
126+
active_ping.stop()
127+
_, _, lost = active_ping.report()
128+
return _compute_stuntime(lost_packets=lost)
129+
130+
131+
def _compute_stuntime(lost_packets: int) -> float:
132+
"""Compute stuntime from lost packet count.
133+
134+
Args:
135+
lost_packets: Number of packets lost during migration.
136+
137+
Returns:
138+
Stuntime in seconds (connectivity gap).
139+
"""
140+
# Add +1 to account for the gap from last successful reply before loss to first successful reply after recovery
141+
stuntime = 0.0 if lost_packets == 0 else (lost_packets + 1) * PING_INTERVAL_SECONDS
142+
LOGGER.info(f"Stuntime: {stuntime:.1f}s (from {lost_packets} lost packets)")
143+
return stuntime

tests/network/localnet/migration_stuntime/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
from libs.vm.vm import BaseVirtualMachine
1212
from tests.network.libs import cloudinit
1313
from tests.network.libs import cluster_user_defined_network as libcudn
14+
from tests.network.libs.stuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing
1415
from tests.network.localnet.liblocalnet import (
1516
GUEST_1ST_IFACE_NAME,
1617
LOCALNET_OVS_BRIDGE_INTERFACE,
1718
ip_addresses_from_pool,
1819
libnncp,
1920
localnet_vm,
2021
)
21-
from tests.network.localnet.migration_stuntime.libstuntime import CLIENT_VM_LABEL, SERVER_VM_LABEL, ContinuousPing
2222

2323
LOGGER = logging.getLogger(__name__)
2424

tests/network/localnet/migration_stuntime/test_migration_stuntime.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
Client - The connectivity initiator VM that runs continuous ping toward the server VM.
1414
Server - The connectivity listener VM that receives the ping and responds.
1515
16-
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main
17-
/stps/sig-network/stuntime_measurement.md
16+
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/stuntime_measurement.md
1817
"""
1918

2019
import pytest
2120

2221
from libs.vm.affinity import new_pod_affinity, new_pod_anti_affinity
23-
from tests.network.localnet.migration_stuntime.libstuntime import (
22+
from tests.network.libs.stuntime import (
2423
CLIENT_VM_LABEL,
2524
SERVER_VM_LABEL,
2625
STUNTIME_THRESHOLD_SECONDS,

0 commit comments

Comments
 (0)