|
| 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() |
0 commit comments