diff --git a/README.md b/README.md index c3e5f57..f06acaf 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ interfaces = owner.get_interfaces() ``` ## Exceptions raised by MFD-Network-Adapter module - related to module: `NetworkAdapterModuleException` -- related to Network Interface: `InterfaceNameNotFound`, `IPException`, `IPAddressesNotFound`, `NetworkQueuesException`, `RDMADeviceNotFound`, `NumaNodeException`, `DriverInfoNotFound`, `FirmwareVersionNotFound` +- related to Network Interface: `InterfaceNameNotFound`, `IPException`, `IPAddressesNotFound`, `NetworkQueuesException`, `RDMADeviceNotFound`, `NumaNodeException`, `DriverInfoNotFound`, `FirmwareVersionNotFound`, `VirtualFunctionNotFoundException`, `HypervisorNotSupportedException`, `NetworkAdapterConfigurationException`, `NetworkInterfaceNotSupported` - related to NetworkInterface's features: `VirtualizationFeatureException` - ## Classes @@ -650,6 +650,11 @@ Value of param will be prepared for update for all interfaces using str ``` +[Linux] Enable or disable SRIOV drivers auto probe. +```python +set_sriov_driver_autoprobe(self, state: bool) -> None +``` + [Windows] `change_state_family_interfaces(*, driver_filename: str, enable: State.ENABLED) -> None` @@ -927,6 +932,16 @@ verify_vmdq(interface: "NetworkInterface", desired_value: int) -> None get_vm_vf_ids(self, vm_name: str, interface: "ESXiNetworkInterface") -> list[int] ``` +[Linux] Set number of MSI-X vectors for PF interface. +```python +set_msix_vectors_count(self, count: int, method: MethodType = MethodType.DEVLINK) -> None +``` + +[Linux] Get number of MSI-X vectors of PF interface. +```python +get_msix_vectors_count(self, method: MethodType = MethodType.DEVLINK) -> int +``` + ### Queue [L] Get number of queues from proc interrupts @@ -1259,6 +1274,8 @@ class NetworkInterface(ABC): - `get_number_of_ports() -> int'` - Get number of ports in tested adapter. +- `reload_adapter_devlink() -> None` - Reload adapter via devlink. + - `restart() -> None` - Restart interface. #### Additional methods - Linux @@ -2032,6 +2049,18 @@ get_queues_for_rss_engine(self) -> dict[str, list[str]] get_netq_defq_rss_queues(self, netq_rss: bool) -> list ``` +[Linux] Set RSS queues count. + +```python +set_rss_queues_count(self, count: int, vf_pci_address: PCIAddress | None = None) -> None +``` + +[Linux] Get RSS queues count. + +```python +get_rss_queues_count(self, vf_pci_address: PCIAddress | None = None) -> int +``` + #### Stats @@ -2258,6 +2287,7 @@ Virtualization related functionalities. - `set_link_for_vf(vf_id: int, link_state: LinkState) -> None` - Set link for a VF interface. - `set_vlan_for_vf(vf_id: int, vlan_id: int, proto: VlanProto) -> None` - Set port VLAN for a VF interface - `set_mac_for_vf(vf_id: int, mac: MACAddress) -> None` - Set MAC address for VF interface. +- `get_vf_id_by_pci(vf_pci_address: PCIAddress) -> int` - Get VF ID based on PCI Address. - `get_max_vfs() -> int` - Get maximal number of VFs per interface based on either name or PCI Address (if name not set on the interface). - `get_current_vfs() -> int` - Get current number of VFs per interface based on either name or PCI Address (if name not set on the interface). - `get_designed_number_vfs() -> tuple[int, int]` - Get designed max number of VFs, total and per PF. diff --git a/mfd_network_adapter/exceptions.py b/mfd_network_adapter/exceptions.py index 9a5a914..d95aade 100644 --- a/mfd_network_adapter/exceptions.py +++ b/mfd_network_adapter/exceptions.py @@ -17,3 +17,19 @@ class NetworkInterfaceIncomparableObject(Exception): class VirtualFunctionCreationException(Exception): """Exception raised when VF creation process fails.""" + + +class VirtualFunctionNotFoundException(Exception): + """Exception raised when VF is not found after creation.""" + + +class HypervisorNotSupportedException(Exception): + """Exception raised when the hypervisor is not supported.""" + + +class NetworkAdapterConfigurationException(Exception): + """Exception raised for errors in network adapter configuration.""" + + +class NetworkInterfaceNotSupported(Exception): + """Exception raised when the operation called on network interface is not supported.""" diff --git a/mfd_network_adapter/network_interface/feature/driver/linux.py b/mfd_network_adapter/network_interface/feature/driver/linux.py index f0fa0b2..486d905 100644 --- a/mfd_network_adapter/network_interface/feature/driver/linux.py +++ b/mfd_network_adapter/network_interface/feature/driver/linux.py @@ -2,12 +2,16 @@ # SPDX-License-Identifier: MIT """Module for Driver feature for Linux.""" +import logging import re from typing import Dict, TYPE_CHECKING +from mfd_common_libs import add_logging_level, log_levels from mfd_const import Speed from mfd_ethtool import Ethtool +from mfd_typing.network_interface import InterfaceType +from mfd_network_adapter.exceptions import NetworkInterfaceNotSupported, NetworkAdapterConfigurationException from .base import BaseFeatureDriver from ...exceptions import DriverInfoNotFound @@ -16,6 +20,9 @@ from mfd_network_adapter import NetworkInterface from mfd_typing.driver_info import DriverInfo +logger = logging.getLogger(__name__) +add_logging_level(level_name="MODULE_DEBUG", level_value=log_levels.MODULE_DEBUG) + class LinuxDriver(BaseFeatureDriver): """Linux class for Driver feature.""" @@ -67,3 +74,28 @@ def get_formatted_driver_version(self) -> Dict: return ver_dict raise DriverInfoNotFound(f"Driver version not available for {self._interface().name}") + + def set_sriov_drivers_autoprobe(self, state: bool) -> None: + """ + Enable or disable SRIOV drivers auto probe. + + :param state: State to set (True or False) + """ + interface = self._interface() + if interface.interface_type is not InterfaceType.PF: + raise NetworkInterfaceNotSupported( + "Enabling/disabling SRIOV drivers auto probe is only supported on PF interface." + ) + status = "enabled" if state else "disabled" + logger.log( + level=log_levels.MFD_DEBUG, + msg=f"{status} sriov_drivers_autoprobe on interface {interface.name}", + ) + self._connection.execute_command( + f"echo {int(state)} > /sys/class/net/{interface.name}/device/sriov_drivers_autoprobe", + custom_exception=NetworkAdapterConfigurationException, + ) + logger.log( + level=log_levels.MFD_INFO, + msg=f"Successfully {status} sriov_drivers_autoprobe on interface {interface.name}", + ) diff --git a/mfd_network_adapter/network_interface/feature/rss/linux.py b/mfd_network_adapter/network_interface/feature/rss/linux.py index 1b78ecb..945511f 100644 --- a/mfd_network_adapter/network_interface/feature/rss/linux.py +++ b/mfd_network_adapter/network_interface/feature/rss/linux.py @@ -10,7 +10,11 @@ from mfd_common_libs import add_logging_level, log_levels from mfd_ethtool import Ethtool from mfd_ethtool.exceptions import EthtoolExecutionError +from mfd_typing import PCIAddress +from mfd_typing.network_interface import InterfaceType + from mfd_network_adapter.data_structures import State +from mfd_network_adapter.exceptions import NetworkInterfaceNotSupported, NetworkAdapterConfigurationException from mfd_network_adapter.stat_checker.base import Trend from .base import BaseFeatureRSS @@ -285,3 +289,54 @@ def validate_statistics(self, traffic_duration: int = 30) -> None: # in ethtool then get_stats will throw a RuntimeError. That is expected behavior. # If it does exist, it should be zero. logger.log(level=log_levels.MODULE_DEBUG, msg=f"Not found {stat_name} statistic, it's expected.") + + def set_rss_queues_count(self, count: int, vf_pci_address: PCIAddress | None = None) -> None: + """ + Set number of RSS queues for the given interface. + + :param vf_pci_address: PCI address of VF to set RSS queues count for. If not provided, sets for PF. + :param count: Number of RSS queues to set + """ + interface = self._interface() + if interface.interface_type is not InterfaceType.PF: + raise NetworkInterfaceNotSupported("Setting RSS queues count on VF is only supported through PF interface.") + logger.log( + level=log_levels.MFD_DEBUG, + msg=f"Setting RSS queues count to {count} for {'VF' if vf_pci_address else interface.name} interface.", + ) + vf_path = "" + if vf_pci_address: + vf_num = str(interface.virtualization.get_vf_id_by_pci(vf_pci_address)) + vf_path = f"virtfn{vf_num}/" + self._connection.execute_command( + f"echo {count} > /sys/class/net/{interface.name}/device/{vf_path}rss_lut_pf_attr", + custom_exception=NetworkAdapterConfigurationException, + ) + logger.log( + level=log_levels.MFD_INFO, + msg=f"Successfully set RSS queues count on interface {interface.name} to {count}", + ) + + def get_rss_queues_count(self, vf_pci_address: PCIAddress | None = None) -> int: + """ + Get number of RSS queues of the given interface. + + :param vf_pci_address: PCI address of VF to get RSS queues count for. If not provided, gets for PF. + :return: Number of RSS queues + """ + interface = self._interface() + if interface.interface_type is not InterfaceType.PF: + raise NetworkInterfaceNotSupported("Getting RSS queues count on VF is only supported through PF interface.") + logger.log( + level=log_levels.MFD_DEBUG, + msg=f"Retrieving RSS queues count of {'VF' if vf_pci_address else interface.name} interface.", + ) + vf_path = "" + if vf_pci_address: + vf_num = str(interface.virtualization.get_vf_id_by_pci(vf_pci_address)) + vf_path = f"virtfn{vf_num}/" + out = self._connection.execute_command( + f"cat /sys/class/net/{interface.name}/device/{vf_path}rss_lut_pf_attr", + expected_return_codes={0}, + ).stdout + return int(out) diff --git a/mfd_network_adapter/network_interface/feature/virtualization/data_structures.py b/mfd_network_adapter/network_interface/feature/virtualization/data_structures.py index 54b2e22..a6fbf51 100644 --- a/mfd_network_adapter/network_interface/feature/virtualization/data_structures.py +++ b/mfd_network_adapter/network_interface/feature/virtualization/data_structures.py @@ -13,3 +13,11 @@ class VFInfo: vf_id: str pci_address: PCIAddress owner_world_id: str + + +@dataclass +class MethodType: + """Class for method types.""" + + DEVLINK: str = "devlink" + SYSFS: str = "sysfs" diff --git a/mfd_network_adapter/network_interface/feature/virtualization/linux.py b/mfd_network_adapter/network_interface/feature/virtualization/linux.py index 71dd24a..3db7fe8 100644 --- a/mfd_network_adapter/network_interface/feature/virtualization/linux.py +++ b/mfd_network_adapter/network_interface/feature/virtualization/linux.py @@ -7,11 +7,18 @@ from typing import List from mfd_common_libs import add_logging_level, log_levels -from mfd_typing import MACAddress, DeviceID, SubDeviceID -from mfd_typing.network_interface import InterfaceType from mfd_const.network import DESIGNED_NUMBER_VFS_BY_SPEED, Speed +from mfd_typing import MACAddress, DeviceID, SubDeviceID, PCIAddress +from mfd_typing.network_interface import InterfaceType + from mfd_network_adapter.data_structures import State +from mfd_network_adapter.exceptions import ( + VirtualFunctionNotFoundException, + NetworkAdapterConfigurationException, + NetworkInterfaceNotSupported, +) from .base import BaseFeatureVirtualization +from .data_structures import MethodType from ...data_structures import VlanProto, VFDetail, LinkState from ...exceptions import VirtualizationFeatureException, VirtualizationWrongInterfaceException, DeviceSetupException @@ -338,3 +345,89 @@ def set_mac_for_vf(self, vf_id: int, mac: MACAddress) -> None: self._raise_error_if_not_supported_type() cmd = f"ip link set {self._interface().name} vf {vf_id} mac {mac}" self._connection.execute_command(command=cmd, custom_exception=VirtualizationFeatureException) + + def get_vf_id_by_pci(self, vf_pci_address: PCIAddress) -> int: + """ + Get ID of VF with the given PCI address on specific PF PCI address using /sys/bus/pci/devices/pci_address. + + :param vf_pci_address: VF interface PCI address. + :return: ID of the VF. + """ + result = self._connection.execute_command( + f"ls /sys/bus/pci/devices/{self._interface().pci_address}/virtfn* -la", + shell=True, + expected_return_codes={0, 1}, + ) + if result.return_code != 0: + raise VirtualFunctionNotFoundException( + f"Failed to list VFs for PF PCI Address {self._interface().pci_address}: {result.stderr.strip()}" + ) + vf_number_regex = rf"^.*devices/{self._interface().pci_address}/virtfn(?P\d+).*->.*{vf_pci_address}$" + match = re.search(vf_number_regex, result.stdout, re.M) + if match: + return int(match.group("vf_number")) + else: + raise VirtualFunctionNotFoundException(f"0 matched VFs for PF PCI Address {self._interface().pci_address}") + + def get_msix_vectors_count(self, method: MethodType = MethodType.DEVLINK) -> int: + """ + Get number of MSI-X vectors for the given interface. + + :param method: Method to use for setting MSI-X vectors count. Options are "devlink" or "sysfs". + :return: Number of MSI-X vectors available for the interface. + """ + interface = self._interface() + if interface.interface_type is not InterfaceType.PF: + raise NetworkInterfaceNotSupported("Getting MSI-X vector count is only supported on PF interface.") + logger.log(level=log_levels.MFD_DEBUG, msg=f"Getting MSI-X vectors count for interface {interface.name}") + if method == MethodType.DEVLINK: + out = self._connection.execute_command(f"devlink resource show pci/{interface.pci_address}").stdout + match = re.search(r"name msix_vf size (\d+) ", out) + if match: + logger.log( + level=log_levels.MFD_INFO, + msg=f"MSI-X vectors count for interface {interface.name}: {match.group(1)}", + ) + return int(match.group(1)) + else: + raise NetworkAdapterConfigurationException( + f"Could not find MSI-X vectors count for interface {interface.name}" + ) + + if method == MethodType.SYSFS: + out = self._connection.execute_command( + f"cat /sys/bus/pci/devices/{self._interface().pci_address}/sriov_vf_msix_count" + ).stdout + if out: + logger.log(level=log_levels.MFD_INFO, msg=f"MSI-X vectors count for interface {interface.name}: {out}") + return int(out) + else: + raise NetworkAdapterConfigurationException( + f"Could not find MSI-X vectors count for interface {interface.name}" + ) + + raise ValueError(f"Unknown method {method} for getting MSI-X vectors count") + + def set_msix_vectors_count(self, count: int, method: MethodType = MethodType.DEVLINK) -> None: + """ + Set number of MSI-X vectors for the given interface. + + :param count: Number of MSI-X vectors to set + :param method: Method to use for setting MSI-X vectors count. Options are "devlink" or "sysfs". + """ + interface = self._interface() + if interface.interface_type is not InterfaceType.PF: + raise NetworkInterfaceNotSupported( + "Setting MSI-X vector count on VF is only supported through PF interface." + ) + logger.log( + level=log_levels.MFD_DEBUG, msg=f"Setting MSI-X vectors count to {count} for interface {interface.name}" + ) + if method == MethodType.DEVLINK: + command = f"devlink resource set pci/{interface.pci_address} path /msix/msix_vf/ size {count}" + elif method == MethodType.SYSFS: + command = f"echo {count} > /sys/bus/pci/devices/{interface.pci_address}/sriov_vf_msix_count" + else: + raise ValueError(f"Unknown method {method} for setting MSI-X vectors count") + self._connection.execute_command(command, custom_exception=NetworkAdapterConfigurationException) + logger.log(level=log_levels.MFD_INFO, msg=f"MSI-X vectors count set to {count} for interface {interface.name}") diff --git a/mfd_network_adapter/network_interface/linux.py b/mfd_network_adapter/network_interface/linux.py index 146c661..4455d42 100644 --- a/mfd_network_adapter/network_interface/linux.py +++ b/mfd_network_adapter/network_interface/linux.py @@ -8,6 +8,7 @@ from typing import Dict, Optional, TYPE_CHECKING, Union from mfd_common_libs import add_logging_level, log_levels +from mfd_connect.base import ConnectionCompletedProcess from mfd_connect.exceptions import ConnectionCalledProcessError from mfd_kernel_namespace import add_namespace_call_command from mfd_typing import MACAddress @@ -317,6 +318,17 @@ def get_number_of_ports(self) -> int: else: raise DeviceSetupException("Can't find number of ports in tested adapter.") + def reload_adapter_devlink(self) -> ConnectionCompletedProcess: + """ + Reload adapter using devlink. + + :return: ConnectionCompletedProcess + """ + logger.log(level=log_levels.MFD_DEBUG, msg=f"Reloading adapter {self.name} using devlink") + return self._connection.execute_command( + f"devlink dev reload pci/{self.pci_address}", expected_return_codes={0, 1} + ) + def restart(self) -> None: """Restart interface.""" raise NotImplementedError diff --git a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_driver/test_driver_linux.py b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_driver/test_driver_linux.py index 9506af9..35125ae 100644 --- a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_driver/test_driver_linux.py +++ b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_driver/test_driver_linux.py @@ -3,19 +3,20 @@ """Unit tests for Linux Driver Feature.""" from dataclasses import dataclass +from textwrap import dedent from typing import List import pytest -from textwrap import dedent from mfd_connect import SSHConnection from mfd_connect.base import ConnectionCompletedProcess from mfd_ethtool import Ethtool from mfd_package_manager import LinuxPackageManager from mfd_typing import PCIAddress, OSName from mfd_typing.driver_info import DriverInfo -from mfd_typing.network_interface import LinuxInterfaceInfo -from mfd_network_adapter.network_interface.feature.utils.base import BaseFeatureUtils +from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceType +from mfd_network_adapter.exceptions import NetworkAdapterConfigurationException, NetworkInterfaceNotSupported +from mfd_network_adapter.network_interface.feature.utils.base import BaseFeatureUtils from mfd_network_adapter.network_interface.linux import LinuxNetworkInterface @@ -51,7 +52,38 @@ def interface(self, mocker): mocker.create_autospec(LinuxPackageManager.get_driver_info, return_value=expected_out), ) interface = LinuxNetworkInterface( - connection=conn, interface_info=LinuxInterfaceInfo(pci_address=pci_address, name="eth0") + connection=conn, + interface_info=LinuxInterfaceInfo(pci_address=pci_address, name="eth0", interface_type=InterfaceType.PF), + ) + return interface + + @pytest.fixture() + def vf_interface(self, mocker): + conn = mocker.create_autospec(SSHConnection) + conn.get_os_name.return_value = OSName.LINUX + pci_address = PCIAddress(0, 0, 0, 0) + mocker.patch("mfd_ethtool.Ethtool.check_if_available", mocker.create_autospec(Ethtool.check_if_available)) + mocker.patch( + "mfd_ethtool.Ethtool.get_version", mocker.create_autospec(Ethtool.get_version, return_value="4.15") + ) + mocker.patch( + "mfd_ethtool.Ethtool._get_tool_exec_factory", + mocker.create_autospec(Ethtool._get_tool_exec_factory, return_value="ethtool"), + ) + mocker.patch( + "mfd_ethtool.Ethtool.get_driver_information", + mocker.create_autospec( + Ethtool.get_driver_information, return_value=EthtoolDriverInfo(version=["2.22.18"], driver=["ice"]) + ), + ) + expected_out = DriverInfo(driver_name="ice", driver_version="1.1.1.1") + mocker.patch( + "mfd_package_manager.LinuxPackageManager.get_driver_info", + mocker.create_autospec(LinuxPackageManager.get_driver_info, return_value=expected_out), + ) + interface = LinuxNetworkInterface( + connection=conn, + interface_info=LinuxInterfaceInfo(pci_address=pci_address, name="eth0", interface_type=InterfaceType.VF), ) return interface @@ -83,3 +115,23 @@ def test_get_formatted_driver_version(self, mocker, interface): mocker.create_autospec(BaseFeatureUtils.is_speed_eq, return_value=True), ) assert interface.driver.get_formatted_driver_version() == expected_out + + @pytest.mark.parametrize( + "enable,value,return_code", [(True, "1", 1), (False, "0", 1), (True, "1", 0), (False, "0", 0)] + ) + def test_set_sriov_drivers_autoprobe(self, interface, enable, value, return_code): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=return_code, args="echo", stdout="", stderr="" + ) + interface.driver.set_sriov_drivers_autoprobe(enable) + interface._connection.execute_command.assert_called_once_with( + f"echo {value} > /sys/class/net/{interface.name}/device/sriov_drivers_autoprobe", + custom_exception=NetworkAdapterConfigurationException, + ) + + def test_set_sriov_drivers_autoprobe_exception(self, vf_interface): + with pytest.raises( + NetworkInterfaceNotSupported, + match="Enabling/disabling SRIOV drivers auto probe is only supported on PF interface.", + ): + vf_interface.driver.set_sriov_drivers_autoprobe(False) diff --git a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_rss/test_rss_linux.py b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_rss/test_rss_linux.py index 6d7c3af..64a92f4 100644 --- a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_rss/test_rss_linux.py +++ b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_rss/test_rss_linux.py @@ -9,6 +9,7 @@ from mfd_connect.base import ConnectionCompletedProcess from mfd_ethtool import Ethtool from mfd_network_adapter.data_structures import State +from mfd_network_adapter.exceptions import NetworkAdapterConfigurationException, NetworkInterfaceNotSupported from mfd_network_adapter.network_interface.exceptions import RSSException from mfd_network_adapter.network_interface.feature.link.linux import LinuxLink from mfd_network_adapter.network_interface.feature.rss.linux import LinuxRSS, FlowType @@ -17,7 +18,7 @@ from mfd_network_adapter.stat_checker import StatChecker from mfd_network_adapter.stat_checker.linux import LinuxStatChecker from mfd_typing import OSName, PCIAddress -from mfd_typing.network_interface import LinuxInterfaceInfo +from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceType class TestLinuxNetworkInterface: @@ -184,11 +185,15 @@ def linuxrss(self, mocker): connection.get_os_name.return_value = OSName.LINUX pci_address = PCIAddress(0, 0, 0, 0) interface_10g = LinuxNetworkInterface( - connection=connection, interface_info=LinuxInterfaceInfo(pci_address=pci_address, name="eno1") + connection=connection, + interface_info=LinuxInterfaceInfo(pci_address=pci_address, name="eno1", interface_type=InterfaceType.PF), ) pci_address1 = PCIAddress(0, 0, 0, 1) interface_100g = LinuxNetworkInterface( - connection=connection, interface_info=LinuxInterfaceInfo(pci_address=pci_address1, name="enp59s0f1") + connection=connection, + interface_info=LinuxInterfaceInfo( + pci_address=pci_address1, name="enp59s0f1", interface_type=InterfaceType.VF + ), ) stat_checker = StatChecker(network_interface=interface_100g) yield [interface_10g, interface_100g, stat_checker] @@ -712,3 +717,59 @@ def test_validate_statistics_stat_non_zero(self, linuxrss, mocker): interface_100g.stats.get_per_queue_stat_string.assert_called() interface_100g.rss.get_queues.assert_called_once() interface_100g.stats.get_stats.assert_called() + + @pytest.mark.parametrize("rc", [0, 1]) + def test_set_rss_queues_count_pf(self, linuxrss, rc): + linuxrss[0]._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=rc, args="echo", stdout="", stderr="" + ) + linuxrss[0].rss.set_rss_queues_count(32) + linuxrss[0]._connection.execute_command.assert_called_once_with( + f"echo 32 > /sys/class/net/{linuxrss[0].name}/device/rss_lut_pf_attr", + custom_exception=NetworkAdapterConfigurationException, + ) + + @pytest.mark.parametrize("rc", [0, 1]) + def test_set_rss_queues_count_vf(self, linuxrss, rc, mocker): + mocker.patch.object(linuxrss[0].virtualization, "get_vf_id_by_pci", return_value=1) + + linuxrss[0]._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=rc, args="echo", stdout="", stderr="" + ) + + linuxrss[0].rss.set_rss_queues_count(64, vf_pci_address="0000:00:01.0") + linuxrss[0]._connection.execute_command.assert_called_once_with( + f"echo 64 > /sys/class/net/{linuxrss[0].name}/device/virtfn1/rss_lut_pf_attr", + custom_exception=NetworkAdapterConfigurationException, + ) + + def test_set_rss_queues_count_exception(self, linuxrss): + with pytest.raises( + NetworkInterfaceNotSupported, match="Setting RSS queues count on VF is only supported through PF interface." + ): + linuxrss[1].rss.set_rss_queues_count(32) + + @pytest.mark.parametrize( + "vf_pci_address, vf_num, expected_command", + [ + (None, None, "cat /sys/class/net/eno1/device/rss_lut_pf_attr"), + ("0000:00:01.0", "1", "cat /sys/class/net/eno1/device/virtfn1/rss_lut_pf_attr"), + ], + ) + def test_get_rss_queues_count_success(self, mocker, linuxrss, vf_pci_address, vf_num, expected_command): + mock_execute = mocker.patch.object(linuxrss[0]._connection, "execute_command") + mock_execute.return_value.stdout = "8" + mock_virtualization = mocker.patch.object(linuxrss[0].virtualization, "get_vf_id_by_pci", return_value=vf_num) + + result = linuxrss[0].rss.get_rss_queues_count(vf_pci_address=vf_pci_address) + + assert result == 8 + mock_execute.assert_called_once_with(expected_command, expected_return_codes={0}) + if vf_pci_address: + mock_virtualization.assert_called_once_with(vf_pci_address) + + def test_get_rss_queues_count_exception(self, linuxrss): + with pytest.raises( + NetworkInterfaceNotSupported, match="Getting RSS queues count on VF is only supported through PF interface." + ): + linuxrss[1].rss.get_rss_queues_count() diff --git a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_virtualization/test_virtualization_linux.py b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_virtualization/test_virtualization_linux.py index 6dfd935..6bb7e2e 100644 --- a/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_virtualization/test_virtualization_linux.py +++ b/tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_virtualization/test_virtualization_linux.py @@ -5,23 +5,28 @@ from textwrap import dedent import pytest - from mfd_connect import RPyCConnection from mfd_connect.base import ConnectionCompletedProcess from mfd_typing import PCIAddress, OSName, MACAddress -from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceType +from mfd_typing.mac_address import get_random_mac +from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceType, InterfaceInfo from mfd_network_adapter.data_structures import State +from mfd_network_adapter.exceptions import ( + VirtualFunctionNotFoundException, + NetworkAdapterConfigurationException, + NetworkInterfaceNotSupported, +) from mfd_network_adapter.network_interface.data_structures import VFDetail, LinkState, VlanProto -from mfd_network_adapter.network_interface.linux import LinuxNetworkInterface from mfd_network_adapter.network_interface.exceptions import ( VirtualizationFeatureException, VirtualizationWrongInterfaceException, ) -from mfd_typing.mac_address import get_random_mac +from mfd_network_adapter.network_interface.feature.virtualization.data_structures import MethodType +from mfd_network_adapter.network_interface.linux import LinuxNetworkInterface -class TestQueueLinux: +class TestVirtualizationLinux: @pytest.fixture() def interface(self, mocker): pci_address = PCIAddress(0, 0, 0, 0) @@ -36,6 +41,30 @@ def interface(self, mocker): mocker.stopall() return interface + @pytest.fixture() + def interfaces_with_vf(self, mocker): + connection = mocker.create_autospec(RPyCConnection) + connection.get_os_name.return_value = OSName.LINUX + interfaces = [] + interfaces.append( + LinuxNetworkInterface( + connection=connection, + owner=None, + interface_info=InterfaceInfo(name="eth0", pci_address=PCIAddress(data="0000:18:00.0")), + interface_type=InterfaceType.PF, + ) + ) + interfaces.append( + LinuxNetworkInterface( + connection=connection, + owner=None, + interface_info=InterfaceInfo(name="eth1", pci_address=PCIAddress(data="0000:10:00.0")), + interface_type=InterfaceType.VF, + ) + ) + yield interfaces + mocker.stopall() + def test_set_max_tx_rate(self, interface, mocker): """Test set max tx rate.""" interface.virtualization._raise_error_if_not_supported_type = mocker.Mock() @@ -490,3 +519,145 @@ def test_get_current_vfs_name_unset(self, interface, mocker): interface.virtualization._get_current_vfs_by_pci_address = mocker.Mock() interface.virtualization.get_current_vfs() interface.virtualization._get_current_vfs_by_pci_address.assert_called_once() + + @pytest.mark.parametrize( + "stdout,expected_vf_id", + [ + ( + "root 0 Jan 1 00:00 /sys/bus/pci/devices/0000:18:00.0/virtfn3 -> ../../../0000:10:00.0\n", + 3, + ), + ( + "root root 0 Jan 1 00:00 /sys/bus/pci/devices/0000:18:00.0/virtfn7 -> ../../../0000:10:00.0\n", + 7, + ), + ], + ) + def test_retrieves_correct_vf_id_for_matching_pci_address(self, interfaces_with_vf, stdout, expected_vf_id): + pf, vf = interfaces_with_vf + pf._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="ls", stdout=stdout, stderr="" + ) + assert pf.virtualization.get_vf_id_by_pci(vf.pci_address) == expected_vf_id + + def test_raises_exception_when_no_matching_vf_found(self, interfaces_with_vf): + pf, vf = interfaces_with_vf + stdout = "root 0 Jan 1 00:00 /sys/bus/pci/devices/0000:00:00.0/virtfn3 -> ../../../0000:de:ad:be.ef\n" + pf._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="ls", stdout=stdout, stderr="" + ) + with pytest.raises( + VirtualFunctionNotFoundException, + match=f"0 matched VFs for PF PCI Address {pf.pci_address}", + ): + pf.virtualization.get_vf_id_by_pci(vf.pci_address) + + def test_raises_exception_when_command_fails(self, interfaces_with_vf): + pf, vf = interfaces_with_vf + pf._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=1, args="ls", stdout="", stderr="Error" + ) + with pytest.raises(VirtualFunctionNotFoundException): + pf.virtualization.get_vf_id_by_pci(vf.pci_address) + + @pytest.mark.parametrize( + "stdout, expected", + [("resource pci/0000:00:00.0:\n name msix_vf size 128 occ 0 unit entry\n", 128)], + ) + def test_get_msix_vectors_count_devlink_success(self, interface, stdout, expected): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="devlink", stdout=stdout, stderr="" + ) + assert interface.virtualization.get_msix_vectors_count(method=MethodType.DEVLINK) == expected + called_cmd = ( + interface._connection.execute_command.call_args.kwargs.get("command") + or interface._connection.execute_command.call_args.args[0] + ) + assert f"devlink resource show pci/{interface.pci_address}" in called_cmd + + @pytest.mark.parametrize( + "stdout", + ["resource pci/0000:00:00.0:\n name something_else size 64 occ 0\n"], + ) + def test_get_msix_vectors_count_devlink_failed(self, interface, stdout): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="devlink", stdout=stdout, stderr="" + ) + with pytest.raises( + NetworkAdapterConfigurationException, + match=f"Could not find MSI-X vectors count for interface {interface.name}", + ): + interface.virtualization.get_msix_vectors_count(method=MethodType.DEVLINK) + + @pytest.mark.parametrize( + "stdout,expected", + [("256\n", 256)], + ) + def test_get_msix_vectors_count_sysfs(self, interface, stdout, expected): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="cat", stdout=stdout, stderr="" + ) + assert interface.virtualization.get_msix_vectors_count(method=MethodType.SYSFS) == expected + called_cmd = ( + interface._connection.execute_command.call_args.kwargs.get("command") + or interface._connection.execute_command.call_args.args[0] + ) + assert f"/sys/bus/pci/devices/{interface.pci_address}/sriov_vf_msix_count" in called_cmd + + @pytest.mark.parametrize( + "stdout", + [("")], + ) + def test_get_msix_vectors_count_sysfs_failed(self, interface, stdout): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=0, args="devlink", stdout=stdout, stderr="" + ) + with pytest.raises( + NetworkAdapterConfigurationException, + match=f"Could not find MSI-X vectors count for interface {interface.name}", + ): + interface.virtualization.get_msix_vectors_count(method=MethodType.SYSFS) + + def test_get_msix_vectors_count_invalid_method(self, interface): + with pytest.raises(ValueError, match="Unknown method"): + interface.virtualization.get_msix_vectors_count(method="invalid") + + def test_get_msix_vectors_count_exception(self, interfaces_with_vf): + with pytest.raises( + NetworkInterfaceNotSupported, + match="Getting MSI-X vector count is only supported on PF interface.", + ): + interfaces_with_vf[1].virtualization.get_msix_vectors_count() + + @pytest.mark.parametrize("rc", [0, 1]) + def test_set_msix_vectors_count_devlink(self, interface, rc): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=rc, args="devlink", stdout="", stderr="" + ) + interface.virtualization.set_msix_vectors_count(256, method=MethodType.DEVLINK) + interface._connection.execute_command.assert_called_once_with( + f"devlink resource set pci/{interface.pci_address} path /msix/msix_vf/ size 256", + custom_exception=NetworkAdapterConfigurationException, + ) + + @pytest.mark.parametrize("rc", [0, 1]) + def test_set_msix_vectors_count_sysfs(self, interface, rc): + interface._connection.execute_command.return_value = ConnectionCompletedProcess( + return_code=rc, args="echo", stdout="", stderr="" + ) + interface.virtualization.set_msix_vectors_count(128, method=MethodType.SYSFS) + interface._connection.execute_command.assert_called_once_with( + f"echo 128 > /sys/bus/pci/devices/{interface.pci_address}/sriov_vf_msix_count", + custom_exception=NetworkAdapterConfigurationException, + ) + + def test_set_msix_vectors_count_invalid_method(self, interface): + with pytest.raises(ValueError, match="Unknown method"): + interface.virtualization.set_msix_vectors_count(64, method="bad_method") + + def test_set_msix_vectors_count_exception(self, interfaces_with_vf): + with pytest.raises( + NetworkInterfaceNotSupported, + match="Setting MSI-X vector count on VF is only supported through PF interface.", + ): + interfaces_with_vf[1].virtualization.set_msix_vectors_count(32) diff --git a/tests/unit/test_mfd_network_adapter/test_network_interface/test_linux_network_interface.py b/tests/unit/test_mfd_network_adapter/test_network_interface/test_linux_network_interface.py index f1b1701..1c176a5 100644 --- a/tests/unit/test_mfd_network_adapter/test_network_interface/test_linux_network_interface.py +++ b/tests/unit/test_mfd_network_adapter/test_network_interface/test_linux_network_interface.py @@ -9,8 +9,9 @@ from mfd_connect.exceptions import ConnectionCalledProcessError from mfd_ethtool import Ethtool from mfd_typing import PCIAddress, OSName, OSBitness -from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceInfo +from mfd_typing.network_interface import LinuxInterfaceInfo, InterfaceInfo, InterfaceType +from mfd_network_adapter.exceptions import NetworkInterfaceIncomparableObject from mfd_network_adapter.network_interface.data_structures import RingBufferSettings, RingBuffer from mfd_network_adapter.network_interface.exceptions import ( BrandingStringException, @@ -20,7 +21,6 @@ RingBufferSettingException, DeviceSetupException, ) -from mfd_network_adapter.exceptions import NetworkInterfaceIncomparableObject from mfd_network_adapter.network_interface.feature.ip import LinuxIP from mfd_network_adapter.network_interface.feature.link import LinuxLink from mfd_network_adapter.network_interface.linux import LinuxNetworkInterface @@ -66,6 +66,30 @@ def interfaces(self, mocker): yield interfaces mocker.stopall() + @pytest.fixture() + def interfaces_with_vf(self, mocker): + connection = mocker.create_autospec(RPyCConnection) + connection.get_os_name.return_value = OSName.LINUX + interfaces = [] + interfaces.append( + LinuxNetworkInterface( + connection=connection, + owner=None, + interface_info=LinuxInterfaceInfo(name="eth0", pci_address=PCIAddress(data="0000:18:00.0")), + interface_type=InterfaceType.PF, + ) + ) + interfaces.append( + LinuxNetworkInterface( + connection=connection, + owner=None, + interface_info=LinuxInterfaceInfo(name="eth1", pci_address=PCIAddress(data="0000:10:00.0")), + interface_type=InterfaceType.VF, + ) + ) + yield interfaces + mocker.stopall() + def test_get_linux_feature_object(self, mocker): pci_address = PCIAddress(0, 0, 0, 0) name = "eth0" @@ -311,3 +335,15 @@ def test__gt__error(self, mocker, interfaces): NetworkInterfaceIncomparableObject, match="Incorrect object passed for comparison with PCIAddress" ): interfaces[0].__gt__("IncorrectObject") + + @pytest.mark.parametrize("return_code", [0, 1]) + def test_reload_adapter_devlink(self, mocker, interface, return_code): + mock_execute = mocker.patch.object(interface._connection, "execute_command") + mock_execute.return_value = ConnectionCompletedProcess( + return_code=return_code, args="devlink", stdout="", stderr="" + ) + result = interface.reload_adapter_devlink() + assert result.return_code == return_code + mock_execute.assert_called_once_with( + f"devlink dev reload pci/{interface.pci_address}", expected_return_codes={0, 1} + )