Skip to content

Commit e42559f

Browse files
authored
net: NAD live update VM localnet test (#5039)
##### What this PR does / why we need it: Implements the NAD reference change test for localnet: verifies that a running VM can switch its secondary localnet network from one VLAN to another without rebooting, and that TCP connectivity is established on the new VLAN. ##### Which issue(s) this PR fixes: - ##### Special notes for reviewer: Usage of shared localnet setup: NNCP and localnet CUDN (as CUDN-VLAN-A) Includes move of shared helpers to be used in several packages like upcoming metrics nad ref change test. ##### jira-ticket: https://redhat.atlassian.net/browse/CNV-80573 <!-- full-ticket-url needs to be provided. This would add a link to the pull request to the jira and close it when the pull request is merged If the task is not tracked by a Jira ticket, just write "NONE". --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Reorganized network connectivity test helpers for better code organization and reusability. * Added new test fixtures for localnet network configuration and baseline connectivity validation. * Enhanced test coverage with configurable runtime commands and improved VM provisioning capabilities. * **Refactoring** * Consolidated network interface naming constants across test modules for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 2fcc828 + 7b6aebe commit e42559f

6 files changed

Lines changed: 214 additions & 27 deletions

File tree

tests/network/l2_bridge/nad_ref_change/lib_helpers.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from copy import deepcopy
21
from typing import Final
32

43
from kubernetes.dynamic import DynamicClient
54

6-
from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status
75
from libs.vm.factory import base_vmspec, fedora_vm
86
from libs.vm.spec import (
97
CloudInitNoCloud,
@@ -75,26 +73,6 @@ def assert_no_connectivity(
7573
)
7674

7775

78-
def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None:
79-
"""Update secondary network NAD references and wait for the change to be fully applied.
80-
81-
Patches the VM spec atomically, then waits for the MigrationRequired condition to
82-
appear (change detected) and disappear (migration completed).
83-
84-
Args:
85-
vm: The virtual machine to update.
86-
nad_name_by_net: Mapping of interface name to new NAD name.
87-
"""
88-
resource_version = vm.vmi.instance.metadata.resourceVersion
89-
networks = deepcopy(vm.template_spec.networks) or []
90-
for network in networks:
91-
if network.name in nad_name_by_net and network.multus:
92-
network.multus.networkName = nad_name_by_net[network.name]
93-
vm.set_networks(networks=networks)
94-
wait_for_vmi_condition_status(vm=vm, condition="MigrationRequired", resource_version=resource_version)
95-
wait_for_no_vmi_condition(vm=vm, condition="MigrationRequired")
96-
97-
9876
def two_secondary_bridge_vm(
9977
namespace: str,
10078
name: str,

tests/network/l2_bridge/nad_ref_change/test_nad_ref_change.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
GUEST_IFACE_2,
2222
assert_connectivity,
2323
assert_no_connectivity,
24-
update_nad_references,
2524
)
25+
from tests.network.libs.nad_ref import update_nad_references
2626

2727

2828
@pytest.mark.usefixtures("baseline_connectivity")

tests/network/libs/nad_ref.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from copy import deepcopy
2+
3+
from libs.net.vmspec import wait_for_no_vmi_condition, wait_for_vmi_condition_status
4+
from libs.vm.vm import BaseVirtualMachine
5+
6+
7+
def update_nad_references(vm: BaseVirtualMachine, nad_name_by_net: dict[str, str]) -> None:
8+
"""Update secondary network NAD references and wait for the change to be fully applied.
9+
10+
Patches the VM spec atomically, then waits for the MigrationRequired condition to
11+
appear (change detected) and disappear (migration completed).
12+
13+
Args:
14+
vm: The virtual machine to update.
15+
nad_name_by_net: Mapping of spec network name to new NAD name.
16+
"""
17+
resource_version = vm.vmi.instance.metadata.resourceVersion
18+
networks = deepcopy(vm.template_spec.networks) or []
19+
for network in networks:
20+
if network.name in nad_name_by_net and network.multus:
21+
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")

tests/network/localnet/liblocalnet.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
GUEST_2ND_IFACE_NAME: Final[str] = "eth1"
3535
GUEST_3RD_IFACE_NAME: Final[str] = "eth2"
3636

37+
IFACE_A_NAME: Final[str] = "localnet-vlan-a"
38+
IFACE_B_NAME: Final[str] = "localnet-vlan-b"
39+
CUDN_B_NAME: Final[str] = "cudn-localnet-vlan-b"
40+
3741
LOGGER = logging.getLogger(__name__)
3842

3943

@@ -65,6 +69,7 @@ def localnet_vm(
6569
networks: list[Network],
6670
interfaces: list[Interface],
6771
network_data: cloudinit.NetworkData | None = None,
72+
runcmd: list[str] | None = None,
6873
affinity: Affinity | None = None,
6974
vm_labels: dict[str, str] | None = None,
7075
) -> BaseVirtualMachine:
@@ -85,6 +90,8 @@ def localnet_vm(
8590
Each Interface should have a name matching a Network, and additional configuration and state.
8691
network_data (cloudinit.NetworkData | None): Cloud-init NetworkData object containing the network
8792
configuration for the VM interfaces. If None, no network configuration is applied via cloud-init.
93+
runcmd (list[str] | None): Commands to run on first boot via cloud-init runcmd. None means no
94+
extra commands are injected.
8895
affinity (Affinity | None): Optional Affinity object for VM scheduling. Controls the VM scheduling
8996
location. If None, no affinity constraints are applied.
9097
vm_labels (dict[str, str] | None): Optional labels to apply to the VM template metadata.
@@ -124,7 +131,7 @@ def localnet_vm(
124131

125132
if network_data is not None:
126133
# Prevents cloud-init from overriding the default OS user credentials
127-
userdata = cloudinit.UserData(users=[])
134+
userdata = cloudinit.UserData(users=[], runcmd=runcmd)
128135
disk, volume = cloudinitdisk_storage(
129136
data=CloudInitNoCloud(
130137
networkData=cloudinit.asyaml(no_cloud=network_data),
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
from libs.net.ip import filter_link_local_addresses, random_cidr_addresses_by_family
9+
from libs.net.vmspec import lookup_iface_status, wait_for_ifaces_status
10+
from libs.vm.spec import Interface, Multus, Network
11+
from libs.vm.vm import BaseVirtualMachine
12+
from tests.network.libs import cloudinit
13+
from tests.network.libs import cluster_user_defined_network as libcudn
14+
from tests.network.libs.connectivity import ARP_ISOLATION_SYSCTL_CMD, poll_tcp_connectivity
15+
from tests.network.localnet.liblocalnet import (
16+
CUDN_B_NAME,
17+
GUEST_1ST_IFACE_NAME,
18+
GUEST_2ND_IFACE_NAME,
19+
IFACE_A_NAME,
20+
IFACE_B_NAME,
21+
LOCALNET_BR_EX_NETWORK,
22+
LOCALNET_TEST_LABEL,
23+
localnet_cudn,
24+
localnet_vm,
25+
)
26+
27+
NET_SEED: Final[int] = 0
28+
29+
30+
@pytest.fixture(scope="module")
31+
def cudn_nad_ref_vlan_b(
32+
admin_client: DynamicClient,
33+
cudn_localnet: libcudn.ClusterUserDefinedNetwork,
34+
vlan_index_number: Generator[int],
35+
) -> Generator[libcudn.ClusterUserDefinedNetwork]:
36+
with localnet_cudn(
37+
name=CUDN_B_NAME,
38+
match_labels=LOCALNET_TEST_LABEL,
39+
vlan_id=next(vlan_index_number),
40+
physical_network_name=LOCALNET_BR_EX_NETWORK,
41+
client=admin_client,
42+
) as cudn:
43+
cudn.wait_for_status_success()
44+
yield cudn
45+
46+
47+
@pytest.fixture(scope="module")
48+
def ref_vm_localnet(
49+
namespace_localnet_1: Namespace,
50+
unprivileged_client: DynamicClient,
51+
cudn_localnet: libcudn.ClusterUserDefinedNetwork,
52+
cudn_nad_ref_vlan_b: libcudn.ClusterUserDefinedNetwork,
53+
) -> Generator[BaseVirtualMachine]:
54+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=1)
55+
iface_b_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=2)
56+
with localnet_vm(
57+
namespace=namespace_localnet_1.name,
58+
name="ref-vm",
59+
client=unprivileged_client,
60+
networks=[
61+
Network(name=IFACE_A_NAME, multus=Multus(networkName=cudn_localnet.name)),
62+
Network(name=IFACE_B_NAME, multus=Multus(networkName=cudn_nad_ref_vlan_b.name)),
63+
],
64+
interfaces=[
65+
Interface(name=IFACE_A_NAME, bridge={}),
66+
Interface(name=IFACE_B_NAME, bridge={}),
67+
],
68+
network_data=cloudinit.NetworkData(
69+
ethernets={
70+
GUEST_1ST_IFACE_NAME: cloudinit.EthernetDevice(addresses=iface_a_ips),
71+
GUEST_2ND_IFACE_NAME: cloudinit.EthernetDevice(addresses=iface_b_ips),
72+
}
73+
),
74+
runcmd=ARP_ISOLATION_SYSCTL_CMD,
75+
) as vm:
76+
vm.start(wait=True)
77+
vm.wait_for_agent_connected()
78+
wait_for_ifaces_status(
79+
vm=vm,
80+
ip_addresses_by_spec_net_name={
81+
IFACE_A_NAME: [addr.split("/")[0] for addr in iface_a_ips],
82+
IFACE_B_NAME: [addr.split("/")[0] for addr in iface_b_ips],
83+
},
84+
)
85+
yield vm
86+
87+
88+
@pytest.fixture()
89+
def under_test_vm_localnet(
90+
namespace_localnet_1: Namespace,
91+
unprivileged_client: DynamicClient,
92+
cudn_localnet: libcudn.ClusterUserDefinedNetwork,
93+
) -> Generator[BaseVirtualMachine]:
94+
iface_a_ips = random_cidr_addresses_by_family(net_seed=NET_SEED, host_address=3)
95+
with localnet_vm(
96+
namespace=namespace_localnet_1.name,
97+
name="under-test-vm",
98+
client=unprivileged_client,
99+
networks=[Network(name=IFACE_A_NAME, multus=Multus(networkName=cudn_localnet.name))],
100+
interfaces=[Interface(name=IFACE_A_NAME, bridge={})],
101+
network_data=cloudinit.NetworkData(
102+
ethernets={GUEST_1ST_IFACE_NAME: cloudinit.EthernetDevice(addresses=iface_a_ips)},
103+
),
104+
) as vm:
105+
vm.start(wait=True)
106+
vm.wait_for_agent_connected()
107+
wait_for_ifaces_status(
108+
vm=vm,
109+
ip_addresses_by_spec_net_name={
110+
IFACE_A_NAME: [addr.split("/")[0] for addr in iface_a_ips],
111+
},
112+
)
113+
yield vm
114+
115+
116+
@pytest.fixture()
117+
def baseline_connectivity_localnet(
118+
under_test_vm_localnet: BaseVirtualMachine,
119+
ref_vm_localnet: BaseVirtualMachine,
120+
) -> None:
121+
for server_ip in filter_link_local_addresses(
122+
ip_addresses=lookup_iface_status(vm=ref_vm_localnet, iface_name=IFACE_A_NAME).ipAddresses
123+
):
124+
poll_tcp_connectivity(
125+
client_vm=under_test_vm_localnet,
126+
server_vm=ref_vm_localnet,
127+
server_ip=str(server_ip),
128+
server_bind_dev=GUEST_1ST_IFACE_NAME,
129+
)
130+
for server_ip in filter_link_local_addresses(
131+
ip_addresses=lookup_iface_status(vm=ref_vm_localnet, iface_name=IFACE_B_NAME).ipAddresses
132+
):
133+
poll_tcp_connectivity(
134+
client_vm=under_test_vm_localnet,
135+
server_vm=ref_vm_localnet,
136+
server_ip=str(server_ip),
137+
server_bind_dev=GUEST_2ND_IFACE_NAME,
138+
expect_connectivity=False,
139+
)

tests/network/localnet/nad_ref_change/test_nad_ref_change.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,26 @@
1212

1313
import pytest
1414

15+
from libs.net.ip import filter_link_local_addresses
16+
from libs.net.vmspec import lookup_iface_status
17+
from tests.network.libs.connectivity import poll_tcp_connectivity
18+
from tests.network.libs.nad_ref import update_nad_references
19+
from tests.network.localnet.liblocalnet import (
20+
GUEST_1ST_IFACE_NAME,
21+
GUEST_2ND_IFACE_NAME,
22+
IFACE_A_NAME,
23+
IFACE_B_NAME,
24+
)
1525

26+
27+
@pytest.mark.usefixtures("nncp_localnet", "baseline_connectivity_localnet")
1628
@pytest.mark.polarion("CNV-15948")
17-
def test_running_vm_vlan_change():
29+
def test_running_vm_vlan_change(
30+
subtests,
31+
under_test_vm_localnet,
32+
ref_vm_localnet,
33+
cudn_nad_ref_vlan_b,
34+
):
1835
"""
1936
Test that a running VM can change the VLAN of its secondary localnet network, without rebooting.
2037
The VM should establish TCP connectivity on the new VLAN.
@@ -33,6 +50,28 @@ def test_running_vm_vlan_change():
3350
- Under-test VM eventually has TCP connectivity to the reference VM on NAD-VLAN-B
3451
- Under-test VM has no TCP connectivity to the reference VM on NAD-VLAN-A
3552
"""
53+
update_nad_references(vm=under_test_vm_localnet, nad_name_by_net={IFACE_A_NAME: cudn_nad_ref_vlan_b.name})
3654

37-
38-
test_running_vm_vlan_change.__test__ = False
55+
for server_ip in filter_link_local_addresses(
56+
ip_addresses=lookup_iface_status(vm=ref_vm_localnet, iface_name=IFACE_B_NAME).ipAddresses
57+
):
58+
with subtests.test(msg=f"IPv{server_ip.version} connectivity on {IFACE_B_NAME}"):
59+
poll_tcp_connectivity(
60+
client_vm=under_test_vm_localnet,
61+
server_vm=ref_vm_localnet,
62+
server_ip=str(server_ip),
63+
server_bind_dev=GUEST_2ND_IFACE_NAME,
64+
client_bind_dev=GUEST_1ST_IFACE_NAME,
65+
)
66+
for server_ip in filter_link_local_addresses(
67+
ip_addresses=lookup_iface_status(vm=ref_vm_localnet, iface_name=IFACE_A_NAME).ipAddresses
68+
):
69+
with subtests.test(msg=f"IPv{server_ip.version} no connectivity on {IFACE_A_NAME}"):
70+
poll_tcp_connectivity(
71+
client_vm=under_test_vm_localnet,
72+
server_vm=ref_vm_localnet,
73+
server_ip=str(server_ip),
74+
server_bind_dev=GUEST_1ST_IFACE_NAME,
75+
client_bind_dev=GUEST_1ST_IFACE_NAME,
76+
expect_connectivity=False,
77+
)

0 commit comments

Comments
 (0)