Skip to content

Commit 1cf99bd

Browse files
committed
net: non-migratable VM NAD ref change negative test
Implement test_non_migratable_vm_nad_change_not_applied (CNV-15947): verifies that updating the NAD reference on a non-migratable VM does not apply live — the VM reports a RestartRequired condition and retains connectivity to the original VLAN-A. A blank 20Gi RWO DataVolume is attached via two_secondary_bridge_vm's new data_volume_template parameter, making the VM non-live-migratable. Connectivity to VLAN-A is verified as a fixture precondition. Quarantined via CNV-87878: the RestartRequired condition is not yet wired up in KubeVirt — the wait_for_condition call will be unblocked when the product fix lands. Signed-off-by: Asia Khromov <azhivovk@redhat.com> Assisted-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 8758bf8 commit 1cf99bd

4 files changed

Lines changed: 149 additions & 11 deletions

File tree

tests/network/l2_bridge/nad_ref_change/conftest.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import pytest
44
from kubernetes.dynamic import DynamicClient
5+
from ocp_resources.datavolume import DataVolume
56
from ocp_resources.namespace import Namespace
7+
from pytest_testconfig import config as py_config
68

79
from libs.net.ip import filter_link_local_addresses, random_cidr_addresses_by_family
810
from libs.net.netattachdef import CNIPluginBridgeConfig, NetConfig, NetworkAttachmentDefinition
@@ -145,3 +147,73 @@ def baseline_connectivity(
145147
server_bind_dev=GUEST_IFACE_2,
146148
expect_connectivity=False,
147149
)
150+
151+
152+
@pytest.fixture()
153+
def non_migratable_under_test_vm(
154+
namespace: Namespace,
155+
unprivileged_client: DynamicClient,
156+
bridge_nad_a: NetworkAttachmentDefinition,
157+
) -> Generator[BaseVirtualMachine]:
158+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=5)
159+
blank_dv = DataVolume(
160+
name="non-migratable-dv",
161+
namespace=namespace.name,
162+
source_dict={"blank": {}},
163+
access_modes="ReadWriteOnce",
164+
size="20Gi",
165+
storage_class=py_config["default_storage_class"],
166+
api_name="storage",
167+
)
168+
blank_dv.to_dict()
169+
dv_template = blank_dv.res
170+
with two_secondary_bridge_vm(
171+
namespace=namespace.name,
172+
name="non-migratable-under-test-vm",
173+
client=unprivileged_client,
174+
nad_names=[bridge_nad_a.name],
175+
ip_addresses=[iface_a_ips],
176+
iface_names=[LINUX_BRIDGE_IFACE_NAME_1],
177+
runcmd=ARP_ISOLATION_SYSCTL_CMD,
178+
data_volume_template=dv_template,
179+
) as vm:
180+
vm.start(wait=True)
181+
vm.wait_for_agent_connected()
182+
wait_for_ifaces_status(
183+
vm=vm,
184+
ip_addresses_by_spec_net_name={
185+
LINUX_BRIDGE_IFACE_NAME_1: [addr.split("/")[0] for addr in iface_a_ips],
186+
},
187+
)
188+
yield vm
189+
190+
191+
@pytest.fixture()
192+
def non_migratable_baseline_connectivity(
193+
ref_vm: BaseVirtualMachine,
194+
non_migratable_under_test_vm: BaseVirtualMachine,
195+
) -> None:
196+
"""Verify baseline connectivity before the NAD reference change.
197+
198+
Asserts that the non-migratable under-test VM can reach the reference VM on VLAN-A
199+
and cannot reach it on VLAN-B before any NAD update is applied.
200+
"""
201+
for server_ip in filter_link_local_addresses(
202+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=LINUX_BRIDGE_IFACE_NAME_1).ipAddresses
203+
):
204+
poll_tcp_connectivity(
205+
client_vm=non_migratable_under_test_vm,
206+
server_vm=ref_vm,
207+
server_ip=str(server_ip),
208+
server_bind_dev=GUEST_IFACE_1,
209+
)
210+
for server_ip in filter_link_local_addresses(
211+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=LINUX_BRIDGE_IFACE_NAME_2).ipAddresses
212+
):
213+
poll_tcp_connectivity(
214+
client_vm=non_migratable_under_test_vm,
215+
server_vm=ref_vm,
216+
server_ip=str(server_ip),
217+
server_bind_dev=GUEST_IFACE_2,
218+
expect_connectivity=False,
219+
)

tests/network/l2_bridge/nad_ref_change/lib_helpers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
from typing import Final
1+
from typing import Any, Final
22

33
from kubernetes.dynamic import DynamicClient
44

55
from libs.vm.factory import base_vmspec, fedora_vm
66
from libs.vm.spec import (
77
CloudInitNoCloud,
8+
DataVolumeRef,
89
Devices,
10+
Disk,
911
Interface,
1012
Multus,
1113
Network,
14+
SpecDisk,
15+
Volume,
1216
)
1317
from libs.vm.vm import BaseVirtualMachine, add_volume_disk, cloudinitdisk_storage
1418
from tests.network.libs import cloudinit
@@ -81,6 +85,7 @@ def two_secondary_bridge_vm(
8185
ip_addresses: list[list[str]],
8286
iface_names: list[str],
8387
runcmd: list[str] | None = None,
88+
data_volume_template: dict[str, Any] | None = None,
8489
) -> BaseVirtualMachine:
8590
"""Create a Fedora VM with a masquerade primary interface and bridge-bound secondary interfaces.
8691
@@ -98,6 +103,8 @@ def two_secondary_bridge_vm(
98103
Each inner list contains one address per supported IP family.
99104
iface_names: Logical interface names for the VM spec, aligned with nad_names.
100105
runcmd: Commands to run on first boot via cloud-init runcmd. None means no extra commands.
106+
data_volume_template: When provided, embeds this DataVolume template in the VM spec and
107+
attaches it as an additional disk. A RWO DataVolume makes the VM non-live-migratable.
101108
"""
102109
spec = base_vmspec()
103110
spec.template.spec.domain.devices = Devices(
@@ -125,5 +132,15 @@ def two_secondary_bridge_vm(
125132
userData=cloudinit.format_cloud_config(userdata=userdata),
126133
)
127134
)
135+
if data_volume_template:
136+
dv_name = data_volume_template["metadata"]["name"]
137+
spec.template.spec = add_volume_disk(
138+
vmi_spec=spec.template.spec,
139+
volume=Volume(name=dv_name, dataVolume=DataVolumeRef(name=dv_name)),
140+
disk=SpecDisk(name=dv_name, disk=Disk(bus="virtio")),
141+
)
128142
spec.template.spec = add_volume_disk(vmi_spec=spec.template.spec, volume=volume, disk=disk)
129-
return fedora_vm(namespace=namespace, name=name, client=client, spec=spec)
143+
vm = fedora_vm(namespace=namespace, name=name, client=client, spec=spec)
144+
if data_volume_template:
145+
vm.body["spec"].setdefault("dataVolumeTemplates", []).append(data_volume_template)
146+
return vm

tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
import pytest
1515

1616
from libs.net.ip import filter_link_local_addresses
17-
from libs.net.vmspec import lookup_iface_status
17+
from libs.net.vmspec import lookup_iface_status, wait_for_vmi_condition_status
1818
from tests.network.l2_bridge.libl2bridge import LINUX_BRIDGE_IFACE_NAME_1, LINUX_BRIDGE_IFACE_NAME_2
1919
from tests.network.l2_bridge.nad_ref_change.lib_helpers import (
2020
GUEST_IFACE_1,
2121
GUEST_IFACE_2,
2222
assert_connectivity,
2323
assert_no_connectivity,
2424
)
25-
from tests.network.libs.nad_ref import update_nad_references
25+
from tests.network.libs.nad_ref import patch_nad_references, update_nad_references
2626

2727

2828
@pytest.mark.usefixtures("baseline_connectivity")
@@ -212,8 +212,16 @@ def test_two_networks(
212212
)
213213

214214

215+
@pytest.mark.jira("CNV-87878", run=False)
216+
@pytest.mark.usefixtures("non_migratable_baseline_connectivity")
215217
@pytest.mark.polarion("CNV-15947")
216-
def test_non_migratable_vm_nad_change_not_applied():
218+
def test_non_migratable_vm_nad_change_not_applied(
219+
subtests,
220+
non_migratable_under_test_vm,
221+
ref_vm,
222+
bridge_nad_a,
223+
bridge_nad_b,
224+
):
217225
"""
218226
[NEGATIVE] Test that changing the NAD reference on a non-migratable VM does not
219227
silently succeed — the VM remains connected to the original network.
@@ -233,6 +241,29 @@ def test_non_migratable_vm_nad_change_not_applied():
233241
- Non-migratable under-test VM retains connectivity to the reference VM on NAD-VLAN-A
234242
- Non-migratable under-test VM has no connectivity to the reference VM on NAD-VLAN-B
235243
"""
236-
237-
238-
test_non_migratable_vm_nad_change_not_applied.__test__ = False
244+
patch_nad_references(
245+
vm=non_migratable_under_test_vm, nad_name_by_net={LINUX_BRIDGE_IFACE_NAME_1: bridge_nad_b.name}
246+
)
247+
wait_for_vmi_condition_status(vm=non_migratable_under_test_vm, condition="RestartRequired")
248+
for server_ip in filter_link_local_addresses(
249+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=LINUX_BRIDGE_IFACE_NAME_1).ipAddresses
250+
):
251+
with subtests.test(msg=f"IPv{server_ip.version} on {bridge_nad_a.name}"):
252+
assert_connectivity(
253+
client_vm=non_migratable_under_test_vm,
254+
server_vm=ref_vm,
255+
server_ip=str(server_ip),
256+
server_bind_dev=GUEST_IFACE_1,
257+
client_bind_dev=GUEST_IFACE_1,
258+
)
259+
for server_ip in filter_link_local_addresses(
260+
ip_addresses=lookup_iface_status(vm=ref_vm, iface_name=LINUX_BRIDGE_IFACE_NAME_2).ipAddresses
261+
):
262+
with subtests.test(msg=f"IPv{server_ip.version} on {bridge_nad_b.name}"):
263+
assert_no_connectivity(
264+
client_vm=non_migratable_under_test_vm,
265+
server_vm=ref_vm,
266+
server_ip=str(server_ip),
267+
server_bind_dev=GUEST_IFACE_2,
268+
client_bind_dev=GUEST_IFACE_1,
269+
)

tests/network/libs/nad_ref.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,28 @@ def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str
1515
nad_name_by_net: Mapping of spec network name to new NAD name.
1616
"""
1717
resource_version = vm.vmi.instance.metadata.resourceVersion
18+
networks = _patch_networks(vm=vm, nad_name_by_net=nad_name_by_net)
19+
vm.set_networks(networks=networks)
20+
wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version)
21+
wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired")
22+
23+
24+
def patch_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None:
25+
"""Patch secondary network NAD references without waiting for the change to be applied.
26+
27+
The caller is responsible for waiting on the appropriate VMI condition after this call.
28+
29+
Args:
30+
vm: The virtual machine to update.
31+
nad_name_by_net: Mapping of spec network name to new NAD name.
32+
"""
33+
networks = _patch_networks(vm=vm, nad_name_by_net=nad_name_by_net)
34+
vm.set_networks(networks=networks)
35+
36+
37+
def _patch_networks(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> list:
1838
networks = deepcopy(vm.template_spec.networks) or []
1939
for network in networks:
2040
if network.name in nad_name_by_net and network.multus:
2141
network.multus.networkName = nad_name_by_net[network.name]
22-
vm.set_networks(networks=networks)
23-
wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version)
24-
wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired")
42+
return networks

0 commit comments

Comments
 (0)