Skip to content

Commit 359f0b5

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 (ref_vm, 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 8304bdb commit 359f0b5

3 files changed

Lines changed: 265 additions & 13 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 filter_link_local_addresses, random_cidr_addresses_by_family
9+
from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition
10+
from libs.net.vmspec import lookup_iface_status, 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+
GUEST_IFACE_1,
15+
GUEST_IFACE_2,
16+
NET_SEED,
17+
VM_IFACE_1,
18+
VM_IFACE_2,
19+
bridge_vm,
20+
)
21+
from tests.network.libs.connectivity import poll_tcp_connectivity
22+
23+
24+
@pytest.fixture(scope="module")
25+
def bridge_nad_a(
26+
admin_client: DynamicClient,
27+
namespace: Namespace,
28+
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
29+
vlan_index_number: Generator[int],
30+
) -> Generator[NetworkAttachmentDefinition]:
31+
bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore
32+
with NetworkAttachmentDefinition(
33+
name="nad-vlan-a",
34+
namespace=namespace.name,
35+
config=NetConfig(
36+
name="nad-vlan-a", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))]
37+
),
38+
client=admin_client,
39+
) as nad:
40+
yield nad
41+
42+
43+
@pytest.fixture(scope="module")
44+
def bridge_nad_b(
45+
admin_client: DynamicClient,
46+
namespace: Namespace,
47+
bridge_nncp: libnncp.NodeNetworkConfigurationPolicy,
48+
vlan_index_number: Generator[int],
49+
) -> Generator[NetworkAttachmentDefinition]:
50+
bridge = bridge_nncp.desired_state_spec.interfaces[0].name # type: ignore[union-attr, index]
51+
with NetworkAttachmentDefinition(
52+
name="nad-vlan-b",
53+
namespace=namespace.name,
54+
config=NetConfig(
55+
name="nad-vlan-b", plugins=[CNIPluginBridgeConfig(bridge=bridge, vlan=next(vlan_index_number))]
56+
),
57+
client=admin_client,
58+
) as nad:
59+
yield nad
60+
61+
62+
@pytest.fixture(scope="module")
63+
def ref_vm(
64+
namespace: Namespace,
65+
unprivileged_client: DynamicClient,
66+
bridge_nad_a: NetworkAttachmentDefinition,
67+
bridge_nad_b: NetworkAttachmentDefinition,
68+
) -> Generator[BaseVirtualMachine]:
69+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=1)
70+
iface_b_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=2)
71+
with bridge_vm(
72+
namespace=namespace.name,
73+
name="ref-vm",
74+
client=unprivileged_client,
75+
nad_names=[bridge_nad_a.name, bridge_nad_b.name],
76+
ip_addresses=[iface_a_ips, iface_b_ips],
77+
iface_names=[VM_IFACE_1, VM_IFACE_2],
78+
runcmd=MULTI_IFACE_ARP_RUNCMD,
79+
) as vm:
80+
vm.start(wait=True)
81+
vm.wait_for_agent_connected()
82+
wait_for_ifaces_status(
83+
vm=vm,
84+
ip_addresses_by_spec_net_name={
85+
VM_IFACE_1: [addr.split("/")[0] for addr in iface_a_ips],
86+
VM_IFACE_2: [addr.split("/")[0] for addr in iface_b_ips],
87+
},
88+
)
89+
yield vm
90+
91+
92+
@pytest.fixture(scope="class")
93+
def under_test_vm_two_ifaces(
94+
namespace: Namespace,
95+
unprivileged_client: DynamicClient,
96+
bridge_nad_a: NetworkAttachmentDefinition,
97+
bridge_nad_b: NetworkAttachmentDefinition,
98+
ref_vm: BaseVirtualMachine,
99+
) -> Generator[BaseVirtualMachine]:
100+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=3)
101+
iface_b_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=4)
102+
with bridge_vm(
103+
namespace=namespace.name,
104+
name="under-test-vm-two-ifaces",
105+
client=unprivileged_client,
106+
nad_names=[bridge_nad_a.name, bridge_nad_a.name],
107+
ip_addresses=[iface_a_ips, iface_b_ips],
108+
iface_names=[VM_IFACE_1, VM_IFACE_2],
109+
runcmd=MULTI_IFACE_ARP_RUNCMD,
110+
) as vm:
111+
vm.start(wait=True)
112+
vm.wait_for_agent_connected()
113+
wait_for_ifaces_status(
114+
vm=vm,
115+
ip_addresses_by_spec_net_name={
116+
VM_IFACE_1: [addr.split("/")[0] for addr in iface_a_ips],
117+
VM_IFACE_2: [addr.split("/")[0] for addr in iface_b_ips],
118+
},
119+
)
120+
for ip in filter_link_local_addresses(
121+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=VM_IFACE_1).ipAddresses
122+
):
123+
poll_tcp_connectivity(
124+
client_vm=vm,
125+
server_vm=ref_vm,
126+
server_ip=str(ip),
127+
client_bind_dev=GUEST_IFACE_1,
128+
server_bind_dev=GUEST_IFACE_1,
129+
)
130+
for ip in filter_link_local_addresses(
131+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=VM_IFACE_2).ipAddresses
132+
):
133+
poll_tcp_connectivity(
134+
client_vm=vm,
135+
server_vm=ref_vm,
136+
server_ip=str(ip),
137+
client_bind_dev=GUEST_IFACE_1,
138+
server_bind_dev=GUEST_IFACE_2,
139+
expect_connectivity=False,
140+
)
141+
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)