diff --git a/lisa/microsoft/testsuites/core/provisioning.py b/lisa/microsoft/testsuites/core/provisioning.py index 62291d9211..d5eeba3a54 100644 --- a/lisa/microsoft/testsuites/core/provisioning.py +++ b/lisa/microsoft/testsuites/core/provisioning.py @@ -462,9 +462,9 @@ def check_sriov(self, log: Logger, node: RemoteNode) -> None: pci_nic_check = True if pci_nic_check: log.info( - f"check_sriov: PCI nic count {len(node_nic_info.get_lower_nics())}" + f"check_sriov: PCI nic count {len(node_nic_info.get_pci_nics())}" ) - assert_that(len(node_nic_info.get_lower_nics())).described_as( - f"VF count inside VM is {len(node_nic_info.get_lower_nics())}," + assert_that(len(node_nic_info.get_pci_nics())).described_as( + f"VF count inside VM is {len(node_nic_info.get_pci_nics())}, " f"actual sriov nic count is {sriov_count}" ).is_equal_to(sriov_count) diff --git a/lisa/microsoft/testsuites/kselftest/kselftest-suite.py b/lisa/microsoft/testsuites/kselftest/kselftest-suite.py index b3ed04265d..46499c7f86 100644 --- a/lisa/microsoft/testsuites/kselftest/kselftest-suite.py +++ b/lisa/microsoft/testsuites/kselftest/kselftest-suite.py @@ -1,10 +1,9 @@ from typing import Any, Dict -from microsoft.testsuites.kselftest.kselftest import Kselftest - from lisa import Node, TestCaseMetadata, TestSuite, TestSuiteMetadata from lisa.testsuite import TestResult, simple_requirement from lisa.util import SkippedException, UnsupportedDistroException +from microsoft.testsuites.kselftest.kselftest import Kselftest @TestSuiteMetadata( diff --git a/lisa/microsoft/testsuites/ltp/ltpsuite.py b/lisa/microsoft/testsuites/ltp/ltpsuite.py index 4f87944561..b4f0c68323 100644 --- a/lisa/microsoft/testsuites/ltp/ltpsuite.py +++ b/lisa/microsoft/testsuites/ltp/ltpsuite.py @@ -4,8 +4,6 @@ from logging import Logger from typing import Any, Dict -from microsoft.testsuites.ltp.ltp import Ltp - from lisa import ( Node, TestCaseMetadata, @@ -18,6 +16,7 @@ from lisa.operating_system import BSD, Windows from lisa.testsuite import TestResult from lisa.tools import Lsblk, Swap +from microsoft.testsuites.ltp.ltp import Ltp @TestSuiteMetadata( diff --git a/lisa/microsoft/testsuites/network/common.py b/lisa/microsoft/testsuites/network/common.py index daea203ec2..7d2e094f88 100644 --- a/lisa/microsoft/testsuites/network/common.py +++ b/lisa/microsoft/testsuites/network/common.py @@ -205,14 +205,14 @@ def _setup_nic_monitoring( dest_synthetic_nic = dest_nic_info.name # Determine which NIC to monitor for packet counts - if source_nic_info.lower and source_nic_info.pci_device_name: + if source_nic_info.is_pci_module_enabled and source_nic_info.pci_device_name: source_pci_nic = source_nic_info.pci_device_name source_nic = source_pci_nic else: source_pci_nic = source_nic_info.name source_nic = source_synthetic_nic - if dest_nic_info.lower and dest_nic_info.pci_device_name: + if dest_nic_info.is_pci_module_enabled and dest_nic_info.pci_device_name: dest_pci_nic = dest_nic_info.pci_device_name dest_nic = dest_pci_nic else: @@ -291,9 +291,9 @@ def sriov_vf_connection_test( # turn off lower device if turn_off_lower: - if source_nic_info.lower: + if source_nic_info.is_pci_module_enabled: source_node.tools[Ip].down(source_pci_nic) - if dest_nic_info.lower: + if dest_nic_info.is_pci_module_enabled: dest_node.tools[Ip].down(dest_pci_nic) # Perform file transfer to test connectivity @@ -309,9 +309,9 @@ def sriov_vf_connection_test( # turn on lower device, if turned off before if turn_off_lower: - if source_nic_info.lower: + if source_nic_info.is_pci_module_enabled: source_node.tools[Ip].up(source_pci_nic) - if dest_nic_info.lower: + if dest_nic_info.is_pci_module_enabled: dest_node.tools[Ip].up(dest_pci_nic) # After testing all NICs, ensure at least one valid pair was tested diff --git a/lisa/microsoft/testsuites/network/networksettings.py b/lisa/microsoft/testsuites/network/networksettings.py index 0693b5e392..a6ea6ea170 100644 --- a/lisa/microsoft/testsuites/network/networksettings.py +++ b/lisa/microsoft/testsuites/network/networksettings.py @@ -100,6 +100,9 @@ class NetworkSettings(TestSuite): ), ) def verify_ringbuffer_settings_change(self, node: Node) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") ethtool = node.tools[Ethtool] try: devices_settings = ethtool.get_all_device_ring_buffer_settings() @@ -188,6 +191,10 @@ def verify_ringbuffer_settings_change(self, node: Node) -> None: requirement=simple_requirement(unsupported_os=[BSD, Windows]), ) def verify_device_channels_change(self, node: Node, log: Logger) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + kernel_ver = node.tools[Uname].get_linux_information().kernel_version if ( isinstance(node.os, Ubuntu) @@ -258,6 +265,10 @@ def verify_device_channels_change(self, node: Node, log: Logger) -> None: priority=1, ) def verify_device_enabled_features(self, node: Node) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + required_features = [ "rx-checksumming", "tx-checksumming", @@ -298,6 +309,10 @@ def verify_device_enabled_features(self, node: Node) -> None: requirement=simple_requirement(unsupported_os=[BSD, Windows]), ) def verify_device_gro_lro_settings_change(self, node: Node, log: Logger) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + ethtool = node.tools[Ethtool] skip_test = True @@ -370,6 +385,10 @@ def verify_device_gro_lro_settings_change(self, node: Node, log: Logger) -> None requirement=simple_requirement(unsupported_os=[BSD, Windows]), ) def verify_device_rss_hash_key_change(self, node: Node, log: Logger) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + uname = node.tools[Uname] linux_info = uname.get_linux_information() @@ -435,6 +454,10 @@ def verify_device_rss_hash_key_change(self, node: Node, log: Logger) -> None: priority=2, ) def verify_device_rx_hash_level_change(self, node: Node, log: Logger) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + ethtool = node.tools[Ethtool] # Run the test for both TCP and UDP @@ -494,6 +517,10 @@ def verify_device_rx_hash_level_change(self, node: Node, log: Logger) -> None: ), ) def verify_device_msg_level_change(self, node: Node, log: Logger) -> None: + # Skip test if no synthetic NICs are available + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + # Check if feature is supported by the kernel self._check_msg_level_change_supported(node) @@ -605,6 +632,11 @@ def verify_device_msg_level_change(self, node: Node, log: Logger) -> None: def verify_device_statistics(self, environment: Environment, log: Logger) -> None: server_node = cast(RemoteNode, environment.nodes[0]) client_node = cast(RemoteNode, environment.nodes[1]) + + # Skip test if no synthetic NICs are available + if not client_node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + ethtool = client_node.tools[Ethtool] self._verify_stats_exists(server_node, client_node) @@ -614,10 +646,10 @@ def verify_device_statistics(self, environment: Environment, log: Logger) -> Non device = client_node.nics.default_nic nic = client_node.nics.get_nic(device) - if nic.lower: + if nic.is_pci_module_enabled: # If AN is enabled on this interface then use VF nic stats. an_enabled = True - device = nic.lower + device = nic.pci_device_name timeout = 300 timer = create_timer() @@ -773,9 +805,10 @@ def _verify_stats_exists( per_vf_queue_stats = 0 for device_stats in devices_statistics: nic = client_node.nics.get_nic(device_stats.interface) - if nic.lower: + if nic.is_pci_module_enabled: try: - device_stats = ethtool.get_device_statistics(nic.lower, True) + device_name = nic.pci_device_name + device_stats = ethtool.get_device_statistics(device_name, True) except UnsupportedOperationException as e: raise SkippedException(e) diff --git a/lisa/microsoft/testsuites/network/sriov.py b/lisa/microsoft/testsuites/network/sriov.py index d643b050f6..da5c48018d 100644 --- a/lisa/microsoft/testsuites/network/sriov.py +++ b/lisa/microsoft/testsuites/network/sriov.py @@ -271,6 +271,15 @@ def verify_sriov_max_vf_connection_max_cpu(self, environment: Environment) -> No ), ) def verify_sriov_disable_enable(self, environment: Environment) -> None: + # Skip test if any node has PCI-only NICs (AN without synthetic pairing) + for node in environment.nodes.list(): + for nic in node.nics.nics.values(): + if nic.is_pci_only_nic: + raise SkippedException( + f"SRIOV disable/enable test not applicable for " + f"PCI-only NIC {nic.name} on node {node.name}." + ) + sriov_disable_enable(environment) @TestCaseMetadata( @@ -290,6 +299,15 @@ def verify_sriov_disable_enable(self, environment: Environment) -> None: ), ) def verify_sriov_disable_enable_pci(self, environment: Environment) -> None: + # Skip test if any node has PCI-only NICs (AN without synthetic pairing) + for node in environment.nodes.list(): + for nic in node.nics.nics.values(): + if nic.is_pci_only_nic: + raise SkippedException( + f"SRIOV disable/enable PCI test not applicable for " + f"PCI-only NIC {nic.name} on node {node.name}." + ) + disable_enable_devices(environment) vm_nics = initialize_nic_info(environment) sriov_basic_test(environment) @@ -314,6 +332,15 @@ def verify_sriov_disable_enable_pci(self, environment: Environment) -> None: ), ) def verify_sriov_disable_enable_on_guest(self, environment: Environment) -> None: + # Skip test if any node has PCI-only NICs (AN without synthetic pairing) + for node in environment.nodes.list(): + for nic in node.nics.nics.values(): + if nic.is_pci_only_nic: + raise SkippedException( + f"SRIOV disable/enable on guest test not applicable " + f"for PCI-only NIC {nic.name} on node {node.name}." + ) + vm_nics = initialize_nic_info(environment) sriov_basic_test(environment) sriov_vf_connection_test(environment, vm_nics, turn_off_lower=True) diff --git a/lisa/microsoft/testsuites/network/stress.py b/lisa/microsoft/testsuites/network/stress.py index 953b68c5d7..128f9e2e4d 100644 --- a/lisa/microsoft/testsuites/network/stress.py +++ b/lisa/microsoft/testsuites/network/stress.py @@ -15,6 +15,7 @@ Environment, Logger, RemoteNode, + SkippedException, TestCaseMetadata, TestSuite, TestSuiteMetadata, @@ -133,6 +134,15 @@ def stress_sriov_iperf(self, environment: Environment) -> None: ), ) def stress_sriov_disable_enable(self, environment: Environment) -> None: + # Skip test if any node has PCI-only NICs (AN without synthetic pairing) + for node in environment.nodes.list(): + for nic in node.nics.nics.values(): + if nic.is_pci_only_nic: + raise SkippedException( + f"SRIOV stress disable/enable test not applicable " + f"for PCI-only NIC {nic.name} on node {node.name}." + ) + sriov_disable_enable(environment, times=50) @TestCaseMetadata( @@ -157,6 +167,11 @@ def stress_sriov_disable_enable(self, environment: Environment) -> None: def stress_synthetic_provision_with_max_nics_reboot( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for _ in range(10): for node in environment.nodes.list(): @@ -185,6 +200,11 @@ def stress_synthetic_provision_with_max_nics_reboot( def stress_synthetic_with_max_nics_reboot_from_platform( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for _ in range(10): for node in environment.nodes.list(): @@ -214,6 +234,11 @@ def stress_synthetic_with_max_nics_reboot_from_platform( def stress_synthetic_with_max_nics_stop_start_from_platform( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for _ in range(10): for node in environment.nodes.list(): diff --git a/lisa/microsoft/testsuites/network/synthetic.py b/lisa/microsoft/testsuites/network/synthetic.py index 3503d73cc5..9550d53e10 100644 --- a/lisa/microsoft/testsuites/network/synthetic.py +++ b/lisa/microsoft/testsuites/network/synthetic.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from lisa import ( Environment, + SkippedException, TestCaseMetadata, TestSuite, TestSuiteMetadata, @@ -42,6 +43,11 @@ class Synthetic(TestSuite): def verify_synthetic_provision_with_max_nics( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) @TestCaseMetadata( @@ -65,6 +71,11 @@ def verify_synthetic_provision_with_max_nics( def verify_synthetic_provision_with_max_nics_reboot( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for node in environment.nodes.list(): node.reboot() @@ -91,6 +102,11 @@ def verify_synthetic_provision_with_max_nics_reboot( def verify_synthetic_provision_with_max_nics_reboot_from_platform( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for node in environment.nodes.list(): start_stop = node.features[StartStop] @@ -118,6 +134,11 @@ def verify_synthetic_provision_with_max_nics_reboot_from_platform( def verify_synthetic_provision_with_max_nics_stop_start_from_platform( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + initialize_nic_info(environment, is_sriov=False) for node in environment.nodes.list(): start_stop = node.features[StartStop] @@ -147,6 +168,11 @@ def verify_synthetic_provision_with_max_nics_stop_start_from_platform( def verify_synthetic_add_max_nics_one_time_after_provision( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + remove_extra_nics(environment) try: for node in environment.nodes.list(): @@ -180,6 +206,11 @@ def verify_synthetic_add_max_nics_one_time_after_provision( def verify_synthetic_add_max_nics_one_by_one_after_provision( self, environment: Environment ) -> None: + # Skip test if no synthetic NICs are available on any node + for node in environment.nodes.list(): + if not node.nics.get_synthetic_devices(): + raise SkippedException("No synthetic NICs available for testing") + remove_extra_nics(environment) try: for node in environment.nodes.list(): diff --git a/lisa/microsoft/testsuites/performance/common.py b/lisa/microsoft/testsuites/performance/common.py index a72d013e18..6574a2ed6c 100644 --- a/lisa/microsoft/testsuites/performance/common.py +++ b/lisa/microsoft/testsuites/performance/common.py @@ -360,8 +360,8 @@ def perf_ntttcp( # noqa: C901 else: need_reboot = False if need_reboot: - client_sriov_count = len(client.nics.get_lower_nics()) - server_sriov_count = len(server.nics.get_lower_nics()) + client_sriov_count = len(client.nics.get_pci_nics()) + server_sriov_count = len(server.nics.get_pci_nics()) for ntttcp in [client_ntttcp, server_ntttcp]: ntttcp.setup_system(udp_mode, set_task_max) for lagscope in [client_lagscope, server_lagscope]: @@ -664,7 +664,7 @@ def check_sriov_count(node: RemoteNode, sriov_count: int) -> None: node_nic_info = node.nics node_nic_info.reload() - assert_that(len(node_nic_info.get_lower_nics())).described_as( - f"VF count inside VM is {len(node_nic_info.get_lower_nics())}," + assert_that(len(node_nic_info.get_pci_nics())).described_as( + f"VF count inside VM is {len(node_nic_info.get_pci_nics())}," f"actual sriov nic count is {sriov_count}" ).is_equal_to(sriov_count) diff --git a/lisa/microsoft/testsuites/power/common.py b/lisa/microsoft/testsuites/power/common.py index 19640c6118..14c4c6b7eb 100644 --- a/lisa/microsoft/testsuites/power/common.py +++ b/lisa/microsoft/testsuites/power/common.py @@ -139,7 +139,7 @@ def _verify_common_hibernation_requirements( node_nic = node.nics node_nic.initialize() - lower_nics_after_hibernation = node_nic.get_lower_nics() + lower_nics_after_hibernation = node_nic.get_pci_nics() upper_nics_after_hibernation = node_nic.get_nic_names() assert_that(len(lower_nics_after_hibernation)).described_as( @@ -162,7 +162,7 @@ def verify_hibernation_by_tool( then verifies hibernation through tool-specific logs and metrics. """ node_nic = node.nics - lower_nics_before_hibernation = node_nic.get_lower_nics() + lower_nics_before_hibernation = node_nic.get_pci_nics() upper_nics_before_hibernation = node_nic.get_nic_names() hibernation_setup_tool = node.tools[HibernationSetup] @@ -266,7 +266,7 @@ def verify_hibernation_by_vm_extension( raise node_nic = node.nics - lower_nics_before_hibernation = node_nic.get_lower_nics() + lower_nics_before_hibernation = node_nic.get_pci_nics() upper_nics_before_hibernation = node_nic.get_nic_names() # Perform hibernation cycle diff --git a/lisa/microsoft/testsuites/xdp/common.py b/lisa/microsoft/testsuites/xdp/common.py index 9ac26431b9..ffb4480257 100644 --- a/lisa/microsoft/testsuites/xdp/common.py +++ b/lisa/microsoft/testsuites/xdp/common.py @@ -146,7 +146,7 @@ def _aggregate_count( patterns: List[Pattern[str]], ) -> int: ethtool = node.tools[Ethtool] - nic_names = [nic.name, nic.lower] + nic_names = [nic.name, nic.pci_device_name] # aggregate xdp drop count by different nic type new_count = -previous_count @@ -167,7 +167,7 @@ def _aggregate_count( log.debug(f"nic {nic_name} not found, need to reload nics") sleep(2) node.nics.reload() - nic_name = node.nics.get_primary_nic().lower + nic_name = node.nics.get_primary_nic().pci_device_name attempts += 1 else: raise e diff --git a/lisa/microsoft/testsuites/xfstests/xfstesting.py b/lisa/microsoft/testsuites/xfstests/xfstesting.py index cf270fc7f1..b3d4238086 100644 --- a/lisa/microsoft/testsuites/xfstests/xfstesting.py +++ b/lisa/microsoft/testsuites/xfstests/xfstesting.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any, Dict, Union, cast -from microsoft.testsuites.xfstests.xfstests import Xfstests - from lisa import ( Logger, Node, @@ -27,7 +25,12 @@ from lisa.sut_orchestrator.azure.platform_ import AzurePlatform from lisa.testsuite import TestResult from lisa.tools import Echo, FileSystem, KernelConfig, Mkfs, Mount, Parted -from lisa.util import BadEnvironmentStateException, LisaException, generate_random_chars +from lisa.util import ( + BadEnvironmentStateException, + LisaException, + generate_random_chars, +) +from microsoft.testsuites.xfstests.xfstests import Xfstests # Global variables # Section : NFS options. diff --git a/lisa/nic.py b/lisa/nic.py index 2d52ecbbe2..190e20991a 100644 --- a/lisa/nic.py +++ b/lisa/nic.py @@ -13,7 +13,7 @@ from assertpy import assert_that from retry import retry -from lisa.tools import Cat, Ip, KernelConfig, Ls, Lspci, Modprobe, Tee +from lisa.tools import Cat, Ip, KernelConfig, Ls, Lspci, Modprobe, Readlink, Tee from lisa.util import InitializableMixin, LisaException, constants, find_groups_in_lines if TYPE_CHECKING: @@ -62,26 +62,34 @@ def __str__(self) -> str: @property def is_pci_module_enabled(self) -> bool: - # nic with paired pci device - if len(self.lower) > 0: + """ + Check if this NIC has Accelerated Networking (AN) enabled. + This covers both paired NICs (with lower device) + and standalone PCI NICs (without lower device). + """ + # Synthetic NIC paired with VF device + if self.lower and self.is_pci_device: + return True + # Primary interface is PCI device itself + elif not self.lower and self.is_pci_device and self.module_name != "hv_netvsc": return True else: - # pci device without paired nic - if self.is_pci_device: - # pci device without accelerated network module - if self.module_name == "hv_netvsc": - return False - else: - # pci device with accelerated network module - return True - else: - # no pci devices - return False + return False @property def is_pci_device(self) -> bool: return len(self.pci_slot) > 0 + @property + def is_pci_only_nic(self) -> bool: + """ + Check if this is an Accelerated Networking (AN) NIC without + synthetic NIC pairing. + """ + return ( + not self.lower and self.is_pci_device and self.module_name != "hv_netvsc" + ) + @property def pci_device_name(self) -> str: if self.is_pci_device: @@ -160,8 +168,31 @@ def is_empty(self) -> bool: def get_unpaired_devices(self) -> List[str]: return [x.name for x in self.nics.values() if not x.lower] - def get_lower_nics(self) -> List[str]: - return [x.lower for x in self.nics.values() if x.lower] + def get_synthetic_devices(self) -> List[str]: + synthetic_devices = [] + self._node.log.debug("Evaluating NICs for synthetic devices:") + for nic in self.nics.values(): + is_synthetic = not nic.lower and not nic.is_pci_device + self._node.log.debug( + f" NIC {nic.name}: lower='{nic.lower}', " + f"is_pci_device={nic.is_pci_device}, " + f"pci_slot='{nic.pci_slot}', module_name='{nic.module_name}', " + f"is_synthetic={is_synthetic}" + ) + if is_synthetic: + synthetic_devices.append(nic.name) + + self._node.log.debug(f"Found synthetic devices: {synthetic_devices}") + return synthetic_devices + + def get_pci_nics(self) -> List[str]: + pci_nics = [] + for nic in self.nics.values(): + if nic.is_pci_only_nic: + pci_nics.append(nic.name) + else: + pci_nics.append(nic.lower) + return pci_nics def is_pci_module_enabled(self) -> bool: return any( @@ -200,10 +231,12 @@ def get_nic_driver(self, nic_name: str) -> str: # get the current driver for the nic from the node # sysfs provides a link to the driver entry at device/driver nic = self.get_nic(nic_name) - cmd = f"readlink -f /sys/class/net/{nic_name}/device/driver" + readlink = self._node.tools[Readlink] # ex return value: # /sys/bus/vmbus/drivers/hv_netvsc - found_link = self._node.execute(cmd, expected_exit_code=0).stdout + found_link = readlink.get_canonical_path( + f"/sys/class/net/{nic_name}/device/driver" + ) assert_that(found_link).described_as( f"sysfs check for NIC device {nic_name} driver returned no output" ).is_not_equal_to("") @@ -313,7 +346,8 @@ def load_nics_info(self, nic_name: Optional[str] = None) -> None: if not nic_name: assert_that(sorted(found_nics)).described_as( f"Could not locate nic info for all nics. " - f"Nic set was {self.nics.keys()} and only found info for {found_nics}" + f"Nic set was {self.nics.keys()} and only found info for " + f"{found_nics}" ).is_equal_to(sorted(self.nics.keys())) def reload(self) -> None: @@ -330,7 +364,7 @@ def check_pci_enabled(self, pci_enabled: bool) -> None: if self.is_pci_module_enabled(): assert_that(pci_enabled).described_as( "AN enablement and pci device are inconsistent" - ).is_equal_to(any(self.get_lower_nics())) + ).is_equal_to(any(self.get_pci_nics())) else: assert_that(self.get_device_slots()).described_as( "pci devices still on the test node." @@ -368,8 +402,9 @@ def _get_nic_names(self) -> List[str]: return non_virtual_nics def _get_nic_uuid(self, nic_name: str) -> str: - full_dev_path = self._node.execute(f"readlink /sys/class/net/{nic_name}/device") - uuid = os.path.basename(full_dev_path.stdout.strip()) + readlink = self._node.tools[Readlink] + full_dev_path = readlink.get_target(f"/sys/class/net/{nic_name}/device") + uuid = os.path.basename(full_dev_path) self._node.log.debug(f"{nic_name} UUID:{uuid}") return uuid @@ -436,7 +471,7 @@ def _load_nics(self) -> None: for nic_name in [ x for x in self._nic_names - if x not in self.nics.keys() and x not in self.get_lower_nics() + if x not in self.nics.keys() and x not in self.get_pci_nics() ]: nic_info = NicInfo(name=nic_name) self.append(nic_info) @@ -446,6 +481,10 @@ def _load_nics(self) -> None: f"find any nics attached to {self._node.name}." ).is_greater_than(0) + # Handle unpaired PCI NICs: try to discover NICs with their PCI devices + # This covers scenarios where NICs operate standalone without synthetic pairing + self._discover_standalone_pci_nics(lspci) + # handle situation when there is no mana driver, but have mana pci devices if self.is_mana_device_present() and not self.is_mana_driver_enabled(): pci_devices = lspci.get_devices_by_type( @@ -476,6 +515,59 @@ def is_mana_device_present(self) -> bool: def is_mana_driver_enabled(self) -> bool: return self._node.tools[KernelConfig].is_enabled("CONFIG_MICROSOFT_MANA") + def _discover_standalone_pci_nics(self, lspci: Lspci) -> None: + """ + Discover standalone PCI NICs by checking device paths for PCI + information. This handles scenarios where NICs operate as standalone + PCI devices without synthetic pairing. + """ + # Get unpaired NICs that might have PCI devices + unpaired_nics = self.get_unpaired_devices() + readlink = self._node.tools[Readlink] + + for nic_name in unpaired_nics: + nic = self.nics[nic_name] + # Skip if already has PCI slot assigned + if nic.pci_slot: + continue + + # Try to find the PCI slot for this NIC by checking its device path + device_path = readlink.get_canonical_path( + f"/sys/class/net/{nic_name}/device", + no_error_log=True, + ) + if device_path: + # Extract PCI slot from device path using lspci tool + # Path format: /sys/devices/.../XXXX:XX:XX.X/net/nicname + pci_slot = lspci.get_pci_slot_from_device_path(device_path) + if pci_slot: + # Get the module name for this PCI device + try: + module_name = lspci.get_used_module(pci_slot) + if module_name: + nic.pci_slot = pci_slot + # For standalone PCI NICs, set module_name directly + # (no lower_module_name since there's no lower device) + nic.module_name = module_name + self._node.log.debug( + f"Associated unpaired NIC {nic_name} " + f"with PCI slot {pci_slot} (module: {module_name})" + ) + else: + self._node.log.debug( + f"Found PCI slot {pci_slot} for NIC {nic_name} " + f"but could not determine module name" + ) + except Exception as e: + self._node.log.debug( + f"Could not get module for PCI slot {pci_slot}: {e}" + ) + else: + self._node.log.debug( + f"Could not extract PCI slot from device path for {nic_name}: " + f"{device_path}" + ) + def _get_default_nic(self) -> None: self.default_nic: str = "" self.default_nic_route: str = "" diff --git a/lisa/tools/__init__.py b/lisa/tools/__init__.py index 583fd0fcd2..b4c71fbf2c 100644 --- a/lisa/tools/__init__.py +++ b/lisa/tools/__init__.py @@ -113,6 +113,7 @@ from .python import Pip, Python from .qemu import Qemu from .qemu_img import QemuImg +from .readlink import Readlink from .reboot import Reboot from .remote_copy import RemoteCopy from .resize_partition import ResizePartition @@ -268,6 +269,7 @@ "Qemu", "QemuImg", "Reboot", + "Readlink", "RemoteCopy", "ResizePartition", "Rpm", diff --git a/lisa/tools/lspci.py b/lisa/tools/lspci.py index a2bdd505c6..0d4be6d5f3 100644 --- a/lisa/tools/lspci.py +++ b/lisa/tools/lspci.py @@ -138,6 +138,12 @@ # Kernel driver in use: mlx5_core\r PATTERN_MODULE_IN_USE = re.compile(r"Kernel driver in use: ([A-Za-z0-9_-]*)", re.M) +# PCI slot pattern for extracting slot from device paths +# Example: /sys/devices/pci0000:00/0000:00:02.0/net/eth0 -> 0000:00:02.0 +PATTERN_PCI_SLOT = re.compile( + r"([a-fA-F0-9]{4}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}\.[a-fA-F0-9])" +) + class PciDevice: def __init__(self, pci_device_raw: str) -> None: @@ -402,6 +408,13 @@ def get_devices_by_vendor_device_id( devices_list.append(device) return devices_list + def get_pci_slot_from_device_path(self, device_path: str) -> Optional[str]: + """ + Extract PCI slot information from a device path. + """ + pci_slot = get_matched_str(device_path, PATTERN_PCI_SLOT) + return pci_slot if pci_slot else None + class LspciBSD(Lspci): _DEVICE_DRIVER_MAPPING: Dict[str, Pattern[str]] = { diff --git a/lisa/tools/readlink.py b/lisa/tools/readlink.py new file mode 100644 index 0000000000..89be886442 --- /dev/null +++ b/lisa/tools/readlink.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from lisa.executable import Tool +from lisa.util import LisaException + + +class Readlink(Tool): + @property + def command(self) -> str: + return "readlink" + + @property + def can_install(self) -> bool: + return False + + def _read_link( + self, + path: str, + canonicalize: bool = False, + force_run: bool = True, + sudo: bool = False, + no_error_log: bool = False, + ) -> str: + """ + Read symbolic link or canonical file name. + """ + args = "-f " if canonicalize else "" + args += path + + result = self.run( + args, + force_run=force_run, + sudo=sudo, + shell=True, + ) + + if result.exit_code != 0: + if not no_error_log: + raise LisaException(f"Failed to read link '{path}': {result.stderr}") + return "" + + return result.stdout.strip() + + def get_target( + self, + path: str, + sudo: bool = False, + no_error_log: bool = False, + ) -> str: + """ + Get the immediate target of a symbolic link (without following further links). + """ + return self._read_link( + path=path, + canonicalize=False, + sudo=sudo, + no_error_log=no_error_log, + ) + + def get_canonical_path( + self, + path: str, + sudo: bool = False, + no_error_log: bool = False, + ) -> str: + """ + Get the canonical absolute path by following all symbolic links + in every component of the given name recursively. + """ + return self._read_link( + path=path, + canonicalize=True, + sudo=sudo, + no_error_log=no_error_log, + )