Skip to content

Commit 36eae7a

Browse files
committed
net, tests, stuntime: Add L2 bridge migration stuntime scenario
Implement stuntime scenario over Linux bridge in which client VM migrates off the server node. This scenario will serve as a baseline for future performance testing, mirroring the existing OVN localnet stuntime coverage. Signed-off-by: Anat Wax <awax@redhat.com> Assisted-by: Claude <noreply@anthropic.com>
1 parent e977cc6 commit 36eae7a

3 files changed

Lines changed: 150 additions & 9 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
@@ -380,6 +380,7 @@ def secondary_network_vm(
380380
secondary_iface_name: str,
381381
secondary_iface_addresses: list[str],
382382
affinity: Affinity | None = None,
383+
labels: dict[str, str] | None = None,
383384
) -> BaseVirtualMachine:
384385
"""Create a Fedora VM with a masquerade primary interface and a secondary Linux bridge interface.
385386
@@ -391,6 +392,7 @@ def secondary_network_vm(
391392
secondary_iface_name: Name of the secondary network interface in the VM spec.
392393
secondary_iface_addresses: CIDR addresses to assign to the secondary interface via cloud-init.
393394
affinity: Optional node or pod affinity rules for scheduling.
395+
labels: Optional labels to apply to the VM template metadata for pod scheduling.
394396
"""
395397
spec = base_vmspec()
396398
spec.template.spec.domain.devices.interfaces = [ # type: ignore
@@ -404,6 +406,11 @@ def secondary_network_vm(
404406
if affinity:
405407
spec.template.spec.affinity = affinity
406408

409+
if labels:
410+
spec.template.metadata = spec.template.metadata or Metadata()
411+
spec.template.metadata.labels = spec.template.metadata.labels or {}
412+
spec.template.metadata.labels.update(labels)
413+
407414
ethernets = {}
408415
primary = primary_iface_cloud_init()
409416
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

0 commit comments

Comments
 (0)