Skip to content

Commit 63216e3

Browse files
committed
add case for attaching device for nat type
xxxx-296622: [attach-device][nat] attach network type interface by attach-device Signed-off-by: nanli <nanli@redhat.com>
1 parent fb4decf commit 63216e3

File tree

3 files changed

+344
-0
lines changed

3 files changed

+344
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
- virtual_network.attach_detach_device.attach_detach_nat_interface:
2+
type = attach_detach_nat_interface
3+
start_vm = no
4+
take_regular_screendumps = no
5+
login_timeout = 240
6+
ping_target = "www.google.com"
7+
iface_type = "network"
8+
test_mac = "52:54:00:b7:0e:66"
9+
test_mtu = "3000"
10+
test_target_dev = "test"
11+
link_up = "up"
12+
test_alias = "ua-2c8c5183-096a-48aa-b36d-58f49b62acd4"
13+
acpi_idx = "6"
14+
coalesce_max = "7"
15+
tune_sndbuf = 1600
16+
check_dev = "${test_target_dev}"
17+
bridge_name = "virbr1"
18+
variants:
19+
- matrix1_qos_mac_mtu_target_link_alias:
20+
matrix_name = "matrix_1"
21+
basic_config = {'type_name': 'network', 'source': {'network': 'default'}, 'mac_address': '${test_mac}', 'model': 'virtio', 'mtu': {'size': '${test_mtu}'}, "link_state": "${link_up}", 'target': {'dev': '${test_target_dev}'}}
22+
bandwidth_alias = {'bandwidth': {'inbound': {'average': '100', 'peak': '400', 'burst': '256'}, 'outbound': {'average': '200', 'peak': '300', 'burst': '256'}}, 'alias': {'name': '${test_alias}'}}
23+
iface_dict = {**${basic_config}, **${bandwidth_alias}}
24+
expected_checks = {'qos': True, 'mac': '${test_mac}', 'mtu': ${test_mtu}, 'target_dev': '${test_target_dev}', 'link_state': '${link_up}', 'alias': '${test_alias}'}
25+
expected_xpaths = [{'element_attrs': ["//interface[@type='network']", "//interface/source[@network='default']", "//interface/mac[@address='${test_mac}']", "//interface/model[@type='virtio']", "//interface/mtu[@size='${test_mtu}']", "//interface/link[@state='${link_up}']", "//interface/target[@dev='${test_target_dev}']", "//interface/alias[@name='${test_alias}']", "//interface/bandwidth/inbound[@average='100'][@peak='400'][@burst='256']", "//interface/bandwidth/outbound[@average='200'][@peak='300'][@burst='256']"]}]
26+
27+
- matrix2_acpi_tune_coalesce_backend_rom:
28+
matrix_name = "matrix_2"
29+
basic_config = {'type_name': 'network', 'source': {'network': 'default'}, 'model': 'virtio', 'target': {'dev': '${test_target_dev}'}}
30+
advanced_config = {'backend': {'tap': '/dev/net/tun', 'vhost': '/dev/vhost-net'}, 'tune': {'sndbuf': ${tune_sndbuf}}, 'coalesce': {'max': '7'}, 'acpi': {'index': '${acpi_idx}'}, 'rom': {'bar': 'on', 'file': '/usr/share/ipxe/1af41000.rom', 'enabled': 'yes'}}
31+
iface_dict = {**${basic_config}, **${advanced_config}}
32+
expected_checks = {'acpi_index': ${acpi_idx}, 'coalesce': ${coalesce_max}, 'backend': 'tap', 'rom': True, 'target_dev': '${test_target_dev}'}
33+
expected_xpaths = [{'element_attrs': ["//interface[@type='network']", "//interface/source[@network='default']", "//interface/model[@type='virtio']", "//interface/target[@dev='${test_target_dev}']", "//interface/backend[@tap='/dev/net/tun'][@vhost='/dev/vhost-net']", "//interface/coalesce/rx/frames[@max='${coalesce_max}']", "//interface/acpi[@index='${acpi_idx}']", "//interface/rom[@bar='on'][@file='/usr/share/ipxe/1af41000.rom'][@enabled='yes']"]}, {'element_attrs':["//interface/tune/sndbuf"], 'text':'${tune_sndbuf}'}]
34+
35+
- matrix3_driver_elements_4vcpus:
36+
matrix_name = "matrix_3"
37+
vm_attrs = {"vcpu": 4}
38+
driver_queues = "4"
39+
rx_queue_size = '1024'
40+
tx_queue_size = '256'
41+
basic_config = {'type_name': 'network', 'source': {'network': 'default'}, 'model': 'virtio', 'target': {'dev': '${test_target_dev}'}}
42+
driver_config = {'driver':{'driver_attr':{'name': 'vhost', 'txmode': 'iothread', 'ioeventfd': 'on', 'event_idx': 'off', 'queues': '${driver_queues}', 'rss': 'on', 'rss_hash_report': 'on', 'page_per_vq': 'on', 'rx_queue_size': '${rx_queue_size}', 'tx_queue_size': '${tx_queue_size}'},'driver_host': {'csum': 'off', 'gso': 'off', 'tso4': 'off', 'tso6': 'off', 'ecn': 'off', 'ufo': 'off', 'mrg_rxbuf': 'off'}, 'driver_guest': {'csum': 'off', 'tso4': 'off', 'tso6': 'off', 'ecn': 'off', 'ufo': 'off'}}}
43+
iface_dict = {**${basic_config}, **${driver_config}}
44+
expected_checks = {'target_dev': '${test_target_dev}', 'offloads': {'tx-tcp6-segmentation': False, 'tcp-segmentation-offload': False, 'tx-tcp-segmentation': False, 'tx-tcp-ecn-segmentation': False, 'tx-checksumming': False}, 'page_per_vq':'on', 'queue_size': {'rx_queue_size': '${rx_queue_size}', 'tx_queue_size': '${tx_queue_size}'}}
45+
expected_xpaths = [{'element_attrs': ["//interface[@type='network']", "//interface/source[@network='default']", "//interface/model[@type='virtio']", "//interface/target[@dev='${test_target_dev}']", "//interface/driver[@name='vhost'][@txmode='iothread'][@ioeventfd='on'][@event_idx='off'][@queues='${driver_queues}'][@rss='on'][@rss_hash_report='on'][@page_per_vq='on'][@rx_queue_size='${rx_queue_size}'][@tx_queue_size='${tx_queue_size}']", "//interface/driver/host[@csum='off'][@gso='off'][@tso4='off'][@tso6='off'][@ecn='off'][@ufo='off'][@mrg_rxbuf='off']", "//interface/driver/guest[@csum='off'][@tso4='off'][@tso6='off'][@ecn='off'][@ufo='off']"]}]
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2+
#
3+
# Copyright Redhat
4+
#
5+
# SPDX-License-Identifier: GPL-2.0
6+
# Author: Nannan Li<nanli@redhat.com>
7+
#
8+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9+
import re
10+
11+
from avocado.utils import process
12+
13+
from virttest import utils_misc
14+
from virttest import utils_net
15+
from virttest import virsh
16+
from virttest.libvirt_xml import vm_xml
17+
from virttest.utils_libvirt import libvirt_vmxml
18+
19+
from provider.interface import interface_base
20+
from provider.virtual_network import network_base
21+
22+
VIRSH_ARGS = {'debug': True, 'ignore_status': False}
23+
24+
25+
def check_guest_internal_value(test, vm, check_item, expected_value, params):
26+
"""
27+
Check guest internal configuration values using existing utility functions
28+
29+
:param test: test instance
30+
:param vm: VM instance
31+
:param check_item: type of check (qos, mac, mtu, target_dev, link_state, alias, acpi_index, coalesce, backend, nwfilter, rom, offloads, page_per_vq, queue_size)
32+
:param expected_value: expected value to verify
33+
:param params: params object
34+
"""
35+
if vm.serial_console is None:
36+
vm.create_serial_console()
37+
vm_session = vm.wait_for_serial_login()
38+
iface_dict = eval(params.get('iface_dict', {}))
39+
vm_iface = interface_base.get_vm_iface(vm_session)
40+
check_dev = params.get("check_dev")
41+
42+
try:
43+
if check_item == "mtu":
44+
test.log.info("Checking MTU configuration in guest")
45+
vm_iface_info = utils_net.get_linux_iface_info(vm_iface, session=vm_session)
46+
host_iface_info = utils_net.get_linux_iface_info(check_dev)
47+
vm_mtu, host_mtu = vm_iface_info.get('mtu'), host_iface_info.get('mtu')
48+
49+
if not vm_mtu or int(vm_mtu) != int(expected_value):
50+
test.fail(f'MTU of interface inside vm should be {expected_value}, not {vm_mtu}')
51+
if not host_mtu or int(host_mtu) != int(expected_value):
52+
test.fail(f'MTU of interface on host should be {expected_value}, not {host_mtu}')
53+
54+
test.log.debug('MTU check inside vm and host PASS')
55+
56+
elif check_item == "mac":
57+
test.log.info("Checking MAC address configuration in guest")
58+
# Use existing utility function to get guest address map
59+
guest_address_map = utils_net.get_guest_address_map(vm_session)
60+
found_mac = False
61+
for mac_addr in guest_address_map.keys():
62+
if mac_addr.lower() == expected_value.lower():
63+
found_mac = True
64+
break
65+
if not found_mac:
66+
test.fail(f'MAC address {expected_value} not found in guest. Found: {list(guest_address_map.keys())}')
67+
test.log.debug('MAC address check inside vm PASS')
68+
69+
elif check_item == "qos":
70+
test.log.info("Checking QoS configuration")
71+
if not utils_net.check_class_rules(
72+
check_dev, '1:1', iface_dict['bandwidth']['inbound']):
73+
test.fail('Class rule check failed')
74+
if not utils_net.check_filter_rules(
75+
check_dev, iface_dict['bandwidth']['outbound']):
76+
test.fail('Filter rule check failed')
77+
test.log.debug('QOS check for vm PASS')
78+
79+
elif check_item == "target_dev":
80+
test.log.info("Checking target device configuration")
81+
result = virsh.domiflist(vm.name, "", debug=True).stdout_text
82+
if check_dev not in result:
83+
test.fail("Expected interface target dev was not found")
84+
85+
cmd = "ip l show %s" % check_dev
86+
# status, output = vm_session.cmd(cmd)
87+
output = process.run(cmd, shell=True)
88+
if not output:
89+
test.fail(f'Target device {expected_value} not found on host')
90+
test.log.debug('Target device check PASS')
91+
92+
elif check_item == "link_state":
93+
test.log.info("Checking link state configuration in guest")
94+
try:
95+
output = vm_session.cmd_output("ethtool %s | grep 'Link detected'" % vm_iface)
96+
expected_link_state = 'yes' if expected_value == 'up' else 'no'
97+
if f'Link detected: {expected_link_state}' not in output:
98+
test.fail(f'Link state should be {expected_value}, but ethtool shows: {output}')
99+
except Exception as e:
100+
test.fail(f'Error checking link state: {e}')
101+
test.log.debug('Link state check inside vm PASS')
102+
103+
elif check_item == "acpi_index":
104+
test.log.info("Checking ACPI index configuration in guest")
105+
try:
106+
interfaces_output = vm_session.cmd_output("ip l show")
107+
expected_iface_name = f"eno{expected_value}"
108+
if expected_iface_name not in interfaces_output:
109+
test.fail(f'Interface with ACPI index {expected_value} ({expected_iface_name}) not found')
110+
except Exception as e:
111+
test.fail(f'Error checking ACPI index: {e}')
112+
test.log.debug('ACPI index check inside vm PASS')
113+
114+
elif check_item == "coalesce":
115+
test.log.info("Checking coalesce configuration in guest")
116+
# Use ethtool to get coalesce info (similar to utils_net.get_channel_info pattern)
117+
try:
118+
output = process.run("ethtool -c %s |grep rx-frames" % check_dev, shell=True).stdout_text
119+
if not re.findall(f"rx-frames:\s+{expected_value}\n", output):
120+
test.fail(f'Coalesce rx-frames should be {expected_value}, but got: {output}')
121+
except Exception as e:
122+
test.fail(f'Error checking coalesce settings: {e}')
123+
test.log.debug('Coalesce check inside vm PASS')
124+
125+
elif check_item == "offloads":
126+
test.log.info("Checking offloads configuration in guest")
127+
try:
128+
guest_output = vm_session.cmd_output("ethtool -k %s" % vm_iface)
129+
host_output = process.run("ethtool -k %s" % check_dev, shell=True).stdout_text
130+
test.log.debug(f"Guest ethtool output: {guest_output}")
131+
test.log.debug(f"Host ethtool output: {host_output}")
132+
133+
for output in [host_output, guest_output]:
134+
for feature, state in expected_value.items():
135+
expected_state = "on" if state else "off"
136+
pattern = rf"{feature}:\s+{expected_state}(?:\s|$|\[)"
137+
if not re.search(pattern, output):
138+
test.fail(f'Offload feature {feature} should be {expected_state}. Output: {output}')
139+
except Exception as e:
140+
test.fail(f'Error checking offload settings: {e}')
141+
test.log.debug('Offloads check inside vm PASS')
142+
143+
elif check_item == "page_per_vq":
144+
test.log.info("Checking page_per_vq configuration in guest")
145+
# When page_per_vq="on", the PCI notify multiplier should be 4K (0x1000=4096)
146+
try:
147+
# Find the Ethernet controller PCI address
148+
lspci_output = vm_session.cmd_output("lspci | grep Eth")
149+
test.log.debug(f"PCI Ethernet devices: {lspci_output}")
150+
151+
# Extract PCI address (e.g., "01:00.0")
152+
pci_match = re.search(r'(\w{2}:\w{2}\.\w)', lspci_output)
153+
if not pci_match:
154+
test.fail("Could not find Ethernet controller PCI address")
155+
pci_addr = pci_match.group(1)
156+
test.log.debug(f"Found Ethernet PCI address: {pci_addr}")
157+
158+
# Check the PCI notify configuration
159+
lspci_verbose_output = vm_session.cmd_output(f"lspci -vvv -s {pci_addr} | grep -i notify -A1")
160+
test.log.debug(f"PCI notify info: {lspci_verbose_output}")
161+
162+
# Look for multiplier value in the notify capability
163+
multiplier_match = re.search(r'multiplier=(\w+)', lspci_verbose_output)
164+
if not multiplier_match:
165+
test.fail("Could not find notify multiplier in PCI configuration")
166+
167+
multiplier_hex = multiplier_match.group(1)
168+
multiplier_decimal = int(multiplier_hex, 16)
169+
test.log.debug(f"Found notify multiplier: {multiplier_hex} (decimal: {multiplier_decimal})")
170+
171+
# When page_per_vq="on", multiplier should be 4K (4096 = 0x1000)
172+
if expected_value == "on":
173+
expected_multiplier = 4096 # 0x1000
174+
if multiplier_decimal != expected_multiplier:
175+
test.fail(f'page_per_vq="on" should set notify multiplier to 4K ({expected_multiplier}), '
176+
f'but got {multiplier_decimal} (0x{multiplier_hex})')
177+
else:
178+
# When page_per_vq="off", multiplier should be smaller (typically 4)
179+
expected_multiplier = 4
180+
if multiplier_decimal != expected_multiplier:
181+
test.fail(f'page_per_vq="off" should set notify multiplier to {expected_multiplier}, '
182+
f'but got {multiplier_decimal} (0x{multiplier_hex})')
183+
184+
except Exception as e:
185+
test.fail(f'Error checking page_per_vq settings: {e}')
186+
test.log.debug('page_per_vq check inside vm PASS')
187+
188+
elif check_item == "queue_size":
189+
test.log.info("Checking queue size configuration in guest")
190+
output = vm_session.cmd_output(f"ethtool -g {vm_iface}")
191+
test.log.debug(f"ethtool -g output: {output}")
192+
193+
for queue_type, expected in [('RX', expected_value.get('rx_queue_size')), ('TX', expected_value.get('tx_queue_size'))]:
194+
actual = int(re.search(rf'{queue_type}:\s*(\d+)', output).group(1))
195+
if actual != int(expected):
196+
test.fail(f'{queue_type} queue size should be {expected}, but got {actual}')
197+
198+
test.log.debug('Queue size check PASS')
199+
200+
finally:
201+
vm_session.close()
202+
203+
204+
def run(test, params, env):
205+
"""
206+
Test hotplug and hot unplug interface with comprehensive validation
207+
208+
Test steps:
209+
1. Start a VM without any interface
210+
2. Hotplug one interface by attach-device
211+
3. Dump the live XML, content should be consistent with interface.xml
212+
4. Login VM, ping outside to verify connectivity
213+
5. Check various interface properties (QoS, MAC, MTU, target dev, link state, alias, ACPI index, coalesce, backend, nwfilter, ROM, offloads)
214+
6. Hot unplug the interface by detach-device with the same XML
215+
7. Check in VM that interface is detached successfully
216+
8. Check live XML that interface XML disappeared
217+
"""
218+
vm_name = params.get('main_vm')
219+
vm = env.get_vm(vm_name)
220+
vmxml_backup = vm_xml.VMXML.new_from_inactive_dumpxml(vm_name)
221+
222+
# Test parameters
223+
vm_attrs = eval(params.get("vm_attrs", "{}"))
224+
iface_dict = eval(params.get('iface_dict', "{}"))
225+
expected_xpaths = eval(params.get('expected_xpaths', "{}"))
226+
expected_checks = eval(params.get('expected_checks', "{}"))
227+
228+
# Get host interface for network setup
229+
if not utils_misc.wait_for(
230+
lambda: utils_net.get_default_gateway(iface_name=True, force_dhcp=True, json=True) is not None, timeout=15):
231+
test.log.error("Cannot get default gateway in 15s")
232+
host_iface = utils_net.get_default_gateway(iface_name=True, force_dhcp=True, json=True).split()[0]
233+
params["host_iface"] = host_iface
234+
params["check_dev"] = expected_checks.get('target_dev', 'test')
235+
params["iface_dict"] = str(iface_dict)
236+
237+
try:
238+
test.log.debug("TEST_SETUP: Prepare VM without interfaces")
239+
if vm_attrs:
240+
vmxml = vm_xml.VMXML.new_from_inactive_dumpxml(vm_name)
241+
vmxml.setup_attrs(**vm_attrs)
242+
libvirt_vmxml.remove_vm_devices_by_type(vm, "interface")
243+
244+
test.log.debug("TEST_STEP 1: Start VM without any interface")
245+
virsh.start(vm_name, **VIRSH_ARGS)
246+
if vm.serial_console is None:
247+
vm.create_serial_console()
248+
vm.wait_for_serial_login(timeout=240).close()
249+
250+
test.log.debug("TEST_STEP 2: Attach device")
251+
iface = libvirt_vmxml.create_vm_device_by_type('interface', iface_dict)
252+
virsh.attach_device(
253+
vm_name, iface.xml, wait_for_event=True, **VIRSH_ARGS)
254+
test.log.debug("Guest xml:%s", vm_xml.VMXML.new_from_dumpxml(vm_name))
255+
256+
test.log.debug("TEST_STEP 3: Dump live XML and validate consistency")
257+
vmxml = vm_xml.VMXML.new_from_dumpxml(vm_name)
258+
libvirt_vmxml.check_guest_xml_by_xpaths(vmxml, expected_xpaths)
259+
260+
test.log.debug("TEST_STEP 4: Login VM and ping outside")
261+
session = vm.wait_for_serial_login(timeout=240)
262+
current_ifaces = utils_net.get_linux_iface_info(session=session)
263+
if len(current_ifaces) != 2:
264+
test.fail(f"Expected 2 interfaces after attach, found: {len(current_ifaces)}")
265+
266+
# Test connectivity
267+
ips = {'outside_ip': params.get('ping_target')}
268+
network_base.ping_check(params, ips, session, force_ipv4=True)
269+
session.close()
270+
271+
test.log.debug("TEST_STEP 5: Check comprehensive interface properties")
272+
for check_item, expected_value in expected_checks.items():
273+
if expected_value:
274+
check_guest_internal_value(test, vm, check_item, expected_value, params)
275+
276+
test.log.debug("TEST_STEP 6: Hot unplug interface using detach-device")
277+
virsh.detach_device(vm_name, iface.xml, wait_for_event=True, **VIRSH_ARGS)
278+
279+
test.log.debug("TEST_STEP 7: Verify interface removal in guest")
280+
session = vm.wait_for_serial_login(timeout=240)
281+
if not utils_misc.wait_for(
282+
lambda: len(utils_net.get_linux_iface_info(session=session)) == 1, timeout=15):
283+
test.fail("Interface should be removed from guest after detach")
284+
test.log.info("Interface successfully removed from guest")
285+
session.close()
286+
287+
test.log.debug("TEST_STEP 8: Verify interface removal from live XML")
288+
final_vmxml = vm_xml.VMXML.new_from_dumpxml(vm_name)
289+
final_interface_devices = final_vmxml.get_devices('interface')
290+
if final_interface_devices:
291+
test.fail("Interface still present in live XML after detach")
292+
test.log.info("Interface successfully removed from live XML")
293+
294+
finally:
295+
test.log.debug("TEST_TEARDOWN: Restoring VM configuration")
296+
if vm.is_alive():
297+
virsh.destroy(vm_name)
298+
vmxml_backup.sync()

provider/virtual_network/network_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def preparation_for_iface(iface_type, params):
386386
network_dict = eval(params.get("network_dict", "[]"))
387387
if network_dict:
388388
libvirt_network.create_or_del_network(network_dict)
389+
virsh.net_list("--all", debug=True)
389390

390391

391392
def cleanup_for_iface(iface_type, params):

0 commit comments

Comments
 (0)