Skip to content

Commit b29d1e1

Browse files
committed
net: NAD live-update metadata preservation test for Linux bridge
STP: https://github.com/RedHatQE/openshift-virtualization-tests-design-docs/blob/main/stps/sig-network/hotpluggable-nad-ref.md Add test_vm_state_iface_info_preserved: verifies that updating the NAD reference on a running VM's secondary Linux bridge network does not alter the guest interface MAC address, name, or IP addresses. Introduces bridge_vm() — a shared VM factory for Linux bridge tests with multiple secondary interfaces — and the supporting fixtures (under_test_vm_two_ifaces, bridge_nad_a/b). Signed-off-by: Asia Khromov <azhivovk@redhat.com> Assisted-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 652483a commit b29d1e1

3 files changed

Lines changed: 210 additions & 13 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from collections.abc import Generator
2+
3+
import pytest
4+
from kubernetes.dynamic import DynamicClient
5+
from ocp_resources.namespace import Namespace
6+
7+
import tests.network.libs.nodenetworkconfigurationpolicy as libnncp
8+
from libs.net.ip import random_cidr_addresses_by_family
9+
from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition
10+
from libs.net.vmspec import wait_for_ifaces_status
11+
from libs.vm.vm import BaseVirtualMachine
12+
from tests.network.l2_bridge.libl2bridge import MULTI_IFACE_ARP_RUNCMD
13+
from tests.network.l2_bridge.nad_ref_change.lib_helpers import (
14+
NET_SEED,
15+
VM_IFACE_1,
16+
VM_IFACE_2,
17+
bridge_vm,
18+
)
19+
20+
21+
@pytest.fixture(scope="module")
22+
def bridge_nad_a(
23+
admin_client: DynamicClient,
24+
namespace: Namespace,
25+
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
26+
vlan_index_number: Generator[int],
27+
) -> Generator[NetworkAttachmentDefinition]:
28+
bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore
29+
with NetworkAttachmentDefinition(
30+
name="nad-vlan-a",
31+
namespace=namespace.name,
32+
config=NetConfig(
33+
name="nad-vlan-a", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))]
34+
),
35+
client=admin_client,
36+
) as nad:
37+
yield nad
38+
39+
40+
@pytest.fixture(scope="module")
41+
def bridge_nad_b(
42+
admin_client: DynamicClient,
43+
namespace: Namespace,
44+
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
45+
vlan_index_number: Generator[int],
46+
) -> Generator[NetworkAttachmentDefinition]:
47+
bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore[union-attr, index]
48+
with NetworkAttachmentDefinition(
49+
name="nad-vlan-b",
50+
namespace=namespace.name,
51+
config=NetConfig(
52+
name="nad-vlan-b", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))]
53+
),
54+
client=admin_client,
55+
) as nad:
56+
yield nad
57+
58+
59+
@pytest.fixture(scope="class")
60+
def under_test_vm_two_ifaces(
61+
namespace: Namespace,
62+
unprivileged_client: DynamicClient,
63+
bridge_nad_a: NetworkAttachmentDefinition,
64+
bridge_nad_b: NetworkAttachmentDefinition,
65+
) -> Generator[BaseVirtualMachine]:
66+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=3)
67+
iface_b_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=4)
68+
with bridge_vm(
69+
namespace=namespace.name,
70+
name="under-test-vm-two-ifaces",
71+
client=unprivileged_client,
72+
nad_names=[bridge_nad_a.name, bridge_nad_a.name],
73+
ip_addresses=[iface_a_ips, iface_b_ips],
74+
iface_names=[VM_IFACE_1, VM_IFACE_2],
75+
runcmd=MULTI_IFACE_ARP_RUNCMD,
76+
) as vm:
77+
vm.start(wait=True)
78+
vm.wait_for_agent_connected()
79+
wait_for_ifaces_status(
80+
vm=vm,
81+
ip_addresses_by_spec_net_name={
82+
VM_IFACE_1: [addr.split("/")[0] for addr in iface_a_ips],
83+
VM_IFACE_2: [addr.split("/")[0] for addr in iface_b_ips],
84+
},
85+
)
86+
yield vm
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Final
2+
3+
from kubernetes.dynamic import DynamicClient
4+
5+
from libs.net.vmspec import lookup_iface_status
6+
from libs.vm.factory import base_vmspec, fedora_vm
7+
from libs.vm.spec import (
8+
CloudInitNoCloud,
9+
Devices,
10+
Interface,
11+
Multus,
12+
Network,
13+
)
14+
from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage
15+
from tests.network.libs import cloudinit
16+
from tests.network.libs.cloudinit import primary_iface_cloud_init
17+
18+
NET_SEED: Final[int] = 0
19+
20+
VM_IFACE_1: Final[str] = "iface-1"
21+
VM_IFACE_2: Final[str] = "iface-2"
22+
23+
GUEST_IFACE_1: Final[str] = "eth1"
24+
GUEST_IFACE_2: Final[str] = "eth2"
25+
26+
27+
def iface_info(vm: BaseVirtualMachine, iface_name: str) -> dict:
28+
iface = lookup_iface_status(vm=vm, iface_name=iface_name)
29+
return {
30+
"name": iface.name,
31+
"macAddress": iface.macAddress,
32+
"ipAddresses": sorted(iface.ipAddresses or []),
33+
}
34+
35+
36+
def bridge_vm(
37+
namespace: str,
38+
name: str,
39+
client: DynamicClient,
40+
nad_names: list[str],
41+
ip_addresses: list[list[str]],
42+
iface_names: list[str],
43+
runcmd: list[str] | None = None,
44+
) -> BaseVirtualMachine:
45+
"""Create a Fedora VM with a masquerade primary interface and bridge-bound secondary interfaces.
46+
47+
Interface layout in guest OS:
48+
eth0 = masquerade (pod network, primary — handles default route and IPv6)
49+
eth1 = first secondary bridge interface
50+
eth2 = second secondary bridge interface (if present)
51+
52+
Args:
53+
namespace: Namespace to deploy the VM in.
54+
name: VM name.
55+
client: Kubernetes dynamic client.
56+
nad_names: NAD names (multus networkName) for the secondary interfaces, in spec order.
57+
ip_addresses: Per-interface CIDR address lists, aligned with nad_names.
58+
Each inner list contains one address per supported IP family.
59+
iface_names: Logical interface names for the VM spec, aligned with nad_names.
60+
"""
61+
spec = base_vmspec()
62+
spec.template.spec.domain.devices = Devices(
63+
interfaces=[
64+
Interface(name="default", masquerade={}),
65+
*[Interface(name=iface_name, bridge={}) for iface_name in iface_names],
66+
]
67+
)
68+
spec.template.spec.networks = [
69+
Network(name="default", pod={}),
70+
*[
71+
Network(name=iface_name, multus=Multus(networkName=nad_name))
72+
for iface_name, nad_name in zip(iface_names, nad_names)
73+
],
74+
]
75+
ethernets = {}
76+
primary = primary_iface_cloud_init()
77+
if primary:
78+
ethernets["eth0"] = primary
79+
for i, addresses in enumerate(ip_addresses):
80+
ethernets[f"eth{i + 1}"] = cloudinit.EthernetDevice(addresses=addresses)
81+
userdata = cloudinit.UserData(users=[], runcmd=runcmd)
82+
disk, volume = cloudinitdisk_storage(
83+
data=CloudInitNoCloud(
84+
networkData=cloudinit.asyaml(no_cloud=cloudinit.NetworkData(ethernets=ethernets)) if ethernets else "",
85+
userData=cloudinit.format_cloud_config(userdata=userdata),
86+
)
87+
)
88+
spec.template.spec = add_volume_disk(vmi_spec=spec.template.spec, volume=volume, disk=disk)
89+
return fedora_vm(namespace=namespace, name=name, client=client, spec=spec)

tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313

1414
import pytest
1515

16+
from tests.network.l2_bridge.nad_ref_change.lib_helpers import (
17+
VM_IFACE_1,
18+
iface_info,
19+
)
20+
from tests.network.libs.connectivity import update_nad_references
21+
1622

1723
@pytest.mark.incremental
1824
class TestRunningVMLinuxBridgeVlanChange:
@@ -25,23 +31,23 @@ class TestRunningVMLinuxBridgeVlanChange:
2531
Initial (both ifaces on VLAN-A):
2632
Under-test VM Reference VM
2733
+-----------+ +-----------+
28-
| iface-1 |---VLAN-A -->| iface-A |
29-
| iface-2 |---VLAN-A -->| iface-A |
30-
+-----------+ | iface-B |
31-
+-----------+
34+
| iface-1 |\\ | |
35+
| | >--VLAN-A --| iface-1 |
36+
| iface-2 |/ | iface-2 |
37+
+-----------+ +-----------+
3238
3339
After first test (iface-1: VLAN-A -> VLAN-B):
3440
Under-test VM Reference VM
3541
+-----------+ +-----------+
36-
| iface-1 |---VLAN-B -->| iface-B |
37-
| iface-2 |---VLAN-A -->| iface-A |
42+
| iface-1 |---VLAN-B -->| iface-2 |
43+
| iface-2 |---VLAN-A -->| iface-1 |
3844
+-----------+ +-----------+
3945
4046
After third test (iface-1: VLAN-B -> VLAN-A, iface-2: VLAN-A -> VLAN-B):
4147
Under-test VM Reference VM
4248
+-----------+ +-----------+
43-
| iface-1 |---VLAN-A -->| iface-A |
44-
| iface-2 |---VLAN-B -->| iface-B |
49+
| iface-1 |---VLAN-A -->| iface-1 |
50+
| iface-2 |---VLAN-B -->| iface-2 |
4551
+-----------+ +-----------+
4652
4753
Preconditions:
@@ -52,10 +58,13 @@ class TestRunningVMLinuxBridgeVlanChange:
5258
- No TCP connectivity between the under-test VM and the reference VM on NAD-VLAN-B
5359
"""
5460

55-
__test__ = False
56-
5761
@pytest.mark.polarion("CNV-15945")
58-
def test_vm_state_iface_info_preserved(self):
62+
def test_vm_state_iface_info_preserved(
63+
self,
64+
under_test_vm_two_ifaces,
65+
bridge_nad_a,
66+
bridge_nad_b,
67+
):
5968
"""
6069
Test that the under-test VM remains running and its secondary network metadata is unchanged
6170
after the NAD reference change.
@@ -75,6 +84,15 @@ def test_vm_state_iface_info_preserved(self):
7584
- Guest first secondary interface MAC address, name, and IP addresses are the same before and after the
7685
NAD reference change
7786
"""
87+
iface_before = iface_info(vm=under_test_vm_two_ifaces, iface_name=VM_IFACE_1)
88+
89+
update_nad_references(vm=under_test_vm_two_ifaces, updates={VM_IFACE_1: bridge_nad_b.name})
90+
under_test_vm_two_ifaces.wait_for_ready_status(status=True)
91+
92+
iface_after = iface_info(vm=under_test_vm_two_ifaces, iface_name=VM_IFACE_1)
93+
assert iface_after == iface_before, (
94+
f"Interface info changed after NAD reference update: before={iface_before}, after={iface_after}"
95+
)
7896

7997
@pytest.mark.polarion("CNV-15972")
8098
def test_connectivity(self):
@@ -87,14 +105,16 @@ def test_connectivity(self):
87105
- Running reference VM with secondary Linux bridge networks connected to NAD-VLAN-A and NAD-VLAN-B
88106
89107
Steps:
90-
1. Poll TCP connection from the under-test VM to the reference VM on NAD-VLAN-A
91-
2. Poll TCP connection from the under-test VM to the reference VM on NAD-VLAN-B
108+
1. Poll TCP connection from the under-test VM to the reference VM on NAD-VLAN-B
109+
2. Poll TCP connection from the under-test VM to the reference VM on NAD-VLAN-A
92110
93111
Expected:
94112
- Under-test VM eventually has TCP connectivity to the reference VM on NAD-VLAN-B
95113
- Under-test VM has no TCP connectivity to the reference VM on NAD-VLAN-A
96114
"""
97115

116+
test_connectivity.__test__ = False
117+
98118
@pytest.mark.polarion("CNV-15946")
99119
def test_two_networks(self):
100120
"""
@@ -121,6 +141,8 @@ def test_two_networks(self):
121141
- Under-test VM second secondary network eventually has TCP connectivity to the reference VM on NAD-VLAN-B
122142
"""
123143

144+
test_two_networks.__test__ = False
145+
124146

125147
@pytest.mark.polarion("CNV-15947")
126148
def test_non_migratable_vm_nad_change_not_applied():

0 commit comments

Comments
 (0)