diff options
| author | Daniil Baturin <daniil@vyos.io> | 2026-02-03 15:34:10 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-03 15:34:10 +0000 |
| commit | ad3b7eddb696727c80cbf711f4e0567b38ea922c (patch) | |
| tree | e9659ed57941a6ddd4b5f1f3cb5bb0391d6e1dce | |
| parent | a12905c3010520db3c1d7e652c90853940be5e75 (diff) | |
| parent | f01ff15b7530053e18bb79cd529a84e3f15b294a (diff) | |
| download | vyos-1x-ad3b7eddb696727c80cbf711f4e0567b38ea922c.tar.gz vyos-1x-ad3b7eddb696727c80cbf711f4e0567b38ea922c.zip | |
Merge pull request #4919 from alexandr-san4ez/T7730-current
interfaces: T7730: Add interrupt coalescing settings for Ethernet interfaces
| -rw-r--r-- | interface-definitions/interfaces_ethernet.xml.in | 307 | ||||
| -rw-r--r-- | python/vyos/ethtool.py | 20 | ||||
| -rw-r--r-- | python/vyos/ifconfig/ethernet.py | 56 | ||||
| -rw-r--r-- | python/vyos/netlink/__init__.py | 14 | ||||
| -rw-r--r-- | python/vyos/netlink/coalesce.py | 363 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 81 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_ethernet.py | 22 |
7 files changed, 863 insertions, 0 deletions
diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in index 819ceb2cb..e9814817f 100644 --- a/interface-definitions/interfaces_ethernet.xml.in +++ b/interface-definitions/interfaces_ethernet.xml.in @@ -221,6 +221,313 @@ </leafNode> </children> </node> + <node name="interrupt-coalescing"> + <properties> + <help>Interrupt coalescing options for the interface</help> + </properties> + <children> + <leafNode name="adaptive-rx"> + <properties> + <help>Enable adaptive receive interrupt coalescing</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="adaptive-tx"> + <properties> + <help>Enable adaptive transmit interrupt coalescing</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rx-usecs"> + <properties> + <help>Delay in microseconds before generating RX interrupt</help> + <valueHelp> + <format>u32:0-16384</format> + <description>RX interrupt coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-frames"> + <properties> + <help>Number of RX frames before generating interrupt</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>RX interrupt coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-usecs-irq"> + <properties> + <help>Delay in microseconds before generating RX interrupt while servicing IRQ</help> + <valueHelp> + <format>u32:0-16384</format> + <description>RX IRQ interrupt coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-frames-irq"> + <properties> + <help>Number of RX frames before generating interrupt while servicing IRQ</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>RX IRQ interrupt coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-usecs"> + <properties> + <help>Delay in microseconds before generating TX interrupt</help> + <valueHelp> + <format>u32:0-16384</format> + <description>TX interrupt coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-frames"> + <properties> + <help>Number of TX frames before generating interrupt</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>TX interrupt coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-usecs-irq"> + <properties> + <help>Delay in microseconds before generating TX interrupt while servicing IRQ</help> + <valueHelp> + <format>u32:0-16384</format> + <description>TX IRQ interrupt coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-frames-irq"> + <properties> + <help>Number of TX frames before generating interrupt while servicing IRQ</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>TX IRQ interrupt coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stats-block-usecs"> + <properties> + <help>Time in microseconds between updating coalescing statistics</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Statistics block interval</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="pkt-rate-low"> + <properties> + <help>Lower packet rate threshold for adaptive coalescing</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Low packet rate threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-usecs-low"> + <properties> + <help>RX coalescing delay (usecs) for low packet rate</help> + <valueHelp> + <format>u32:0-16384</format> + <description>Low-rate RX coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-frame-low"> + <properties> + <help>RX coalescing frames threshold for low packet rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Low-rate RX coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-usecs-low"> + <properties> + <help>TX coalescing delay (usecs) for low packet rate</help> + <valueHelp> + <format>u32:0-16384</format> + <description>Low-rate TX coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-frame-low"> + <properties> + <help>TX coalescing frames threshold for low packet rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Low-rate TX coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="pkt-rate-high"> + <properties> + <help>Upper packet rate threshold for adaptive coalescing</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>High packet rate threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-usecs-high"> + <properties> + <help>RX coalescing delay (usecs) for high packet rate</help> + <valueHelp> + <format>u32:0-16384</format> + <description>High-rate RX coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rx-frame-high"> + <properties> + <help>RX coalescing frames threshold for high packet rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>High-rate RX coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-usecs-high"> + <properties> + <help>TX coalescing delay (usecs) for high packet rate</help> + <valueHelp> + <format>u32:0-16384</format> + <description>High-rate TX coalescing delay</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-frame-high"> + <properties> + <help>TX coalescing frames threshold for high packet rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>High-rate TX coalescing frames threshold</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="sample-interval"> + <properties> + <help>Sampling interval for adaptive coalescing (in seconds)</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Adaptive sampling interval</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="cqe-mode-rx"> + <properties> + <help>Enable RX CQE (Completion Queue Entry) mode</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="cqe-mode-tx"> + <properties> + <help>Enable TX CQE (Completion Queue Entry) mode</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="tx-aggr-max-bytes"> + <properties> + <help>Maximum number of bytes to aggregate before transmitting</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>TX aggregation maximum bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-aggr-max-frames"> + <properties> + <help>Maximum number of frames to aggregate before transmitting</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>TX aggregation maximum frames</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tx-aggr-time-usecs"> + <properties> + <help>Maximum time in microseconds to wait before transmitting aggregated frames</help> + <valueHelp> + <format>u32:0-16384</format> + <description>TX aggregation timeout</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16384"/> + </constraint> + </properties> + </leafNode> + </children> + </node> #include <include/interface/redirect.xml.i> #include <include/interface/vif-s.xml.i> #include <include/interface/vif.xml.i> diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index d0495df43..ea3ed0b7c 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -14,10 +14,12 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import re +import contextlib from json import loads from vyos.utils.network import interface_exists from vyos.utils.process import popen +from vyos.netlink import coalesce # These drivers do not support using ethtool to change the speed, duplex, or # flow control settings @@ -65,6 +67,7 @@ class Ethtool: _driver_name = None _flow_control = None _channels = '' + _coalesce = None def __init__(self, ifname): # Get driver used for interface @@ -122,6 +125,10 @@ class Ethtool: if not bool(err): self._channels = out.lower() + # Get information about NIC coalesce settings + with contextlib.suppress(coalesce.CoalesceError, coalesce.GeneralNetlinkError): + self._coalesce = coalesce.get_coalesce(ifname) + def check_auto_negotiation_supported(self): """ Check if the NIC supports changing auto-negotiation """ return self._base_settings['supports-auto-negotiation'] @@ -235,3 +242,16 @@ class Ethtool: def check_bonding(self) -> bool: """ Check if ethernet drivers supports bonding """ return bool(self.get_driver_name() not in _drivers_without_bonding_support) + + def check_coalesce(self, setting_name=None): + """Check if the NIC supports 'coalesce' parameter(s)""" + + if not self._coalesce: + return False + + return self._coalesce.get(setting_name) is not None if setting_name else True + + def get_coalesce(self): + """Get all 'coalesce' parameters for the interface""" + + return self._coalesce.copy() if self._coalesce else {} diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 70da837b9..fb29c018c 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -19,6 +19,7 @@ from glob import glob from vyos.base import Warning from vyos.ethtool import Ethtool +from vyos.netlink import coalesce from vyos.ifconfig import Section from vyos.ifconfig.interface import Interface from vyos.utils.dict import dict_search @@ -455,6 +456,57 @@ class EthernetIf(Interface): print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output + def set_interrupt_coalescing(self, params: dict): + """ + Apply ethtool coalesce settings to an interface. + + This method configures interrupt coalescing parameters for the interface + using the netlink API. + + Args: + params: dict containing any of the supported keys, e.g.: + - adaptive_rx, adaptive_tx, + - rx_usecs, rx_frames, rx_usecs_irq, rx_frames_irq, + - tx_usecs, tx_frames, tx_usecs_irq, tx_frames_irq, + - stats_block_usecs, + - pkt_rate_low, pkt_rate_high, + - rx_usecs_low, rx_frames_low, + - tx_usecs_low, tx_frames_low, + - rx_usecs_high, rx_frames_high, + - tx_usecs_high, tx_frames_high, + - sample_interval, + - cqe_mode_rx, cqe_mode_tx, + - tx_aggr_max_bytes, tx_aggr_max_frames, tx_aggr_time_usecs. + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_interrupt_coalescing({'rx_usecs': 8, 'tx_usecs': 16}) + """ + + ifname = self.config['ifname'] + output = '' + + # Nothing to apply + if not params: + return None + + # Override boolean parameters to true if they exist and supported by NIC driver + for boolean_param in coalesce.get_all_params(boolean=True): + supported = self.ethtool.check_coalesce(boolean_param) + if supported: + params[boolean_param] = boolean_param in params + + # Update interrupt coalescing parameters + try: + coalesce.set_coalesce(ifname, **params) + except coalesce.CoalesceError as e: + print(f'interrupt coalescing error: {e}') + except coalesce.GeneralNetlinkError as e: + print(f'netlink error: {e}') + + return output + def set_channels(self, rx_tx_comb, queues): """ Example: @@ -543,6 +595,10 @@ class EthernetIf(Interface): for rx_tx, size in config['ring_buffer'].items(): self.set_ring_buffer(rx_tx, size) + # Set coalesce settings for the interface + if 'interrupt_coalescing' in config: + self.set_interrupt_coalescing(config['interrupt_coalescing']) + self.set_switchdev('switchdev' in config) # call base class last diff --git a/python/vyos/netlink/__init__.py b/python/vyos/netlink/__init__.py new file mode 100644 index 000000000..db07bae58 --- /dev/null +++ b/python/vyos/netlink/__init__.py @@ -0,0 +1,14 @@ +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. diff --git a/python/vyos/netlink/coalesce.py b/python/vyos/netlink/coalesce.py new file mode 100644 index 000000000..d54a4dd07 --- /dev/null +++ b/python/vyos/netlink/coalesce.py @@ -0,0 +1,363 @@ +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from pyroute2.netlink import genlmsg +from pyroute2.netlink import NLM_F_ACK +from pyroute2.netlink import NLM_F_REQUEST +from pyroute2.netlink.exceptions import NetlinkError +from pyroute2.netlink.generic import GenericNetlinkSocket +from pyroute2.netlink.generic.ethtool import ETHTOOL_GENL_NAME +from pyroute2.netlink.generic.ethtool import ETHTOOL_GENL_VERSION +from pyroute2.netlink.generic.ethtool import ethtoolheader + +# Netlink message types for coalesce (from Linux kernel) +ETHTOOL_MSG_COALESCE_GET = 0x13 +ETHTOOL_MSG_COALESCE_SET = 0x14 + +# Error codes from the kernel +EINVAL = 0x16 # Invalid argument +EOPNOTSUPP = 0x5F # Operation not supported + +# Mapping between Python attribute names and netlink attribute names, types +# https://docs.kernel.org/networking/ethtool-netlink.html#coalesce-get +# https://git.kernel.org/pub/scm/network/ethtool/ethtool.git/tree/netlink/coalesce.c +COALESCE_NL_ATTRS = { + 'adaptive_rx': 'ETHTOOL_A_COALESCE_USE_ADAPTIVE_RX', + 'adaptive_tx': 'ETHTOOL_A_COALESCE_USE_ADAPTIVE_TX', + 'cqe_mode_rx': 'ETHTOOL_A_COALESCE_USE_CQE_MODE_RX', + 'cqe_mode_tx': 'ETHTOOL_A_COALESCE_USE_CQE_MODE_TX', + 'pkt_rate_high': 'ETHTOOL_A_COALESCE_PKT_RATE_HIGH', + 'pkt_rate_low': 'ETHTOOL_A_COALESCE_PKT_RATE_LOW', + 'rx_frame_high': 'ETHTOOL_A_COALESCE_RX_MAX_FRAMES_HIGH', + 'rx_frame_low': 'ETHTOOL_A_COALESCE_RX_MAX_FRAMES_LOW', + 'rx_frames': 'ETHTOOL_A_COALESCE_RX_MAX_FRAMES', + 'rx_frames_irq': 'ETHTOOL_A_COALESCE_RX_MAX_FRAMES_IRQ', + 'rx_usecs': 'ETHTOOL_A_COALESCE_RX_USECS', + 'rx_usecs_high': 'ETHTOOL_A_COALESCE_RX_USECS_HIGH', + 'rx_usecs_irq': 'ETHTOOL_A_COALESCE_RX_USECS_IRQ', + 'rx_usecs_low': 'ETHTOOL_A_COALESCE_RX_USECS_LOW', + 'sample_interval': 'ETHTOOL_A_COALESCE_RATE_SAMPLE_INTERVAL', + 'stats_block_usecs': 'ETHTOOL_A_COALESCE_STATS_BLOCK_USECS', + 'tx_aggr_max_bytes': 'ETHTOOL_A_COALESCE_TX_AGGR_MAX_BYTES', + 'tx_aggr_max_frames': 'ETHTOOL_A_COALESCE_TX_AGGR_MAX_FRAMES', + 'tx_aggr_time_usecs': 'ETHTOOL_A_COALESCE_TX_AGGR_TIME_USECS', + 'tx_frame_high': 'ETHTOOL_A_COALESCE_TX_MAX_FRAMES_HIGH', + 'tx_frame_low': 'ETHTOOL_A_COALESCE_TX_MAX_FRAMES_LOW', + 'tx_frames': 'ETHTOOL_A_COALESCE_TX_MAX_FRAMES', + 'tx_frames_irq': 'ETHTOOL_A_COALESCE_TX_MAX_FRAMES_IRQ', + 'tx_usecs': 'ETHTOOL_A_COALESCE_TX_USECS', + 'tx_usecs_high': 'ETHTOOL_A_COALESCE_TX_USECS_HIGH', + 'tx_usecs_irq': 'ETHTOOL_A_COALESCE_TX_USECS_IRQ', + 'tx_usecs_low': 'ETHTOOL_A_COALESCE_TX_USECS_LOW', +} + + +class ethtool_coalesce_msg(genlmsg): + """Netlink message structure for coalesce parameters.""" + + ethtoolheader = ethtoolheader + + # https://docs.kernel.org/networking/ethtool-netlink.html#coalesce-get + nla_map = ( + ('ETHTOOL_A_COALESCE_UNSPEC', 'none'), + ('ETHTOOL_A_COALESCE_HEADER', 'ethtoolheader'), + ('ETHTOOL_A_COALESCE_RX_USECS', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_MAX_FRAMES', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_USECS_IRQ', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_MAX_FRAMES_IRQ', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_USECS', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_MAX_FRAMES', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_USECS_IRQ', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_MAX_FRAMES_IRQ', 'uint32'), + ('ETHTOOL_A_COALESCE_STATS_BLOCK_USECS', 'uint32'), + ('ETHTOOL_A_COALESCE_USE_ADAPTIVE_RX', 'uint8'), + ('ETHTOOL_A_COALESCE_USE_ADAPTIVE_TX', 'uint8'), + ('ETHTOOL_A_COALESCE_PKT_RATE_LOW', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_USECS_LOW', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_MAX_FRAMES_LOW', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_USECS_LOW', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_MAX_FRAMES_LOW', 'uint32'), + ('ETHTOOL_A_COALESCE_PKT_RATE_HIGH', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_USECS_HIGH', 'uint32'), + ('ETHTOOL_A_COALESCE_RX_MAX_FRAMES_HIGH', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_USECS_HIGH', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_MAX_FRAMES_HIGH', 'uint32'), + ('ETHTOOL_A_COALESCE_RATE_SAMPLE_INTERVAL', 'uint32'), + ('ETHTOOL_A_COALESCE_USE_CQE_MODE_TX', 'uint8'), + ('ETHTOOL_A_COALESCE_USE_CQE_MODE_RX', 'uint8'), + ('ETHTOOL_A_COALESCE_TX_AGGR_MAX_BYTES', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_AGGR_MAX_FRAMES', 'uint32'), + ('ETHTOOL_A_COALESCE_TX_AGGR_TIME_USECS', 'uint32'), + ) + + @classmethod + def get_nl_attr_type(cls, nl_attr: str) -> str: + """Returns type of attribute using declared 'nla_map' field""" + + for nla_name, nla_type in cls.nla_map: + if nla_name == nl_attr: + return nla_type + + raise ValueError(f'Unknown netlink attribute name: {nl_attr}') + + +GeneralNetlinkError = NetlinkError + + +class CoalesceError(Exception): + """Base exception for coalesce operations""" + + pass + + +class CoalesceNotSupportedParam(CoalesceError): + """Raised when a coalesce parameter is not supported for modification""" + + pass + + +class CoalesceNotSupportedOperation(CoalesceError): + """Raised when a coalesce operation is not supported by NIC driver""" + + pass + + +class CoalesceInvalidValue(CoalesceError): + """Raised when a coalesce parameter value is invalid""" + + pass + + +class CoalesceNetlink(GenericNetlinkSocket): + """ + Interface coalesce management using `pyroute2` with netlink support. + + This class provides functions to read and set interface coalesce parameters + using the ethtool netlink interface, which properly handles "unsupported" values + (shown as `n/a` in `ethtool --show-coalesce` output). + + IMPORTANT: The kernel's ethtool netlink interface returns coalesce parameters + that have non-zero values, but this does NOT mean they are modifiable. The + driver may impose additional constraints: + + 1. Some parameters are read-only (e.g., `rx_usecs` may be reported but not settable) + 2. Some parameters only accept specific values (e.g., `rx_frames` may only accept 1) + 3. The `supported_coalesce_params` bitmask in the driver determines what's truly + supported, but this is not directly queryable via netlink. + + When setting parameters fails with EOPNOTSUPP (0x5F) or EINVAL (0x16), it typically + means the parameter is not modifiable or the value is not accepted by the driver. + """ + + def __init__(self, ifname: str): + super().__init__() + self._bound = False + self._ifname = ifname + + def _ensure_bound(self): + """Ensure the socket is bound to the ethtool generic netlink family""" + + if not self._bound: + self.bind(ETHTOOL_GENL_NAME, ethtool_coalesce_msg) + self._bound = True + + def _get_dev_header(self): + """Create device header for netlink message""" + + return {'attrs': [['ETHTOOL_A_HEADER_DEV_NAME', self._ifname]]} + + def get_coalesce(self): + """ + Get coalesce parameters for an interface using netlink. + + Example: + >>> cn = CoalesceNetlink('eth0') + >>> coalesce = cn.get_coalesce() + >>> print(coalesce) + {'rx_usecs': 3, 'rx_frames': None, 'tx_usecs': None, ... } + """ + self._ensure_bound() + + msg = ethtool_coalesce_msg() + msg['cmd'] = ETHTOOL_MSG_COALESCE_GET + msg['version'] = ETHTOOL_GENL_VERSION + msg['attrs'].append(('ETHTOOL_A_COALESCE_HEADER', self._get_dev_header())) + + try: + response = self.nlm_request( + msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST + ) + except NetlinkError as e: + if e.code == EOPNOTSUPP: + raise CoalesceNotSupportedOperation( + f'Coalesce operation for {self._ifname} not supported by driver' + ) from e + raise + + if not response: + raise CoalesceError(f'No response for coalesce get on {self._ifname}') + + nl_msg = response[0] + + # Parse response - only include attributes that were returned + # (unsupported attributes won't be in the response) + result = {} + for py_attr, nl_attr in COALESCE_NL_ATTRS.items(): + value = nl_msg.get_attr(nl_attr) # Will be None if not present/supported + + if value is not None: + nl_type = ethtool_coalesce_msg.get_nl_attr_type(nl_attr) + + # Convert to boolean value if it is 'uint8' type + if nl_type == 'uint8': + value = bool(value) + + result[py_attr] = value + + return result + + def set_coalesce(self, **kwargs): + """ + Set coalesce parameters for an interface using netlink. + + Only the parameters that are explicitly passed will be set. + Unsupported parameters will raise an error from the kernel. + + Args: + **kwargs: Coalesce parameters to set. Valid keys are: + - rx_usecs, rx_frames, rx_usecs_irq, rx_frames_irq + - tx_usecs, tx_frames, tx_usecs_irq, tx_frames_irq + - stats_block_usecs + - adaptive_rx, adaptive_tx + - pkt_rate_low, pkt_rate_high + - rx_usecs_low, rx_frame_low, tx_usecs_low, tx_frame_low + - rx_usecs_high, rx_frame_high, tx_usecs_high, tx_frame_high + - sample_interval + - cqe_mode_tx, cqe_mode_rx + - tx_aggr_max_bytes, tx_aggr_max_frames, tx_aggr_time_usecs + + Example: + >>> cn = CoalesceNetlink('eth0') + >>> cn.set_coalesce(rx_usecs=10, tx_usecs=10) + """ + self._ensure_bound() + + msg = ethtool_coalesce_msg() + msg['cmd'] = ETHTOOL_MSG_COALESCE_SET + msg['version'] = ETHTOOL_GENL_VERSION + msg['attrs'].append(('ETHTOOL_A_COALESCE_HEADER', self._get_dev_header())) + + # Add only the parameters that were explicitly provided + for py_attr, value in kwargs.items(): + if py_attr not in COALESCE_NL_ATTRS: + raise CoalesceInvalidValue(f'Unknown coalesce parameter: {py_attr}') + + if value is not None: + nl_attr = COALESCE_NL_ATTRS[py_attr] + nl_type = ethtool_coalesce_msg.get_nl_attr_type(nl_attr) + + # Convert values to 'uint' type + if nl_type.startswith('uint'): + value = int(value) + + msg['attrs'].append((nl_attr, value)) + + try: + self.nlm_request( + msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST | NLM_F_ACK + ) + except NetlinkError as e: + params_str = ', '.join( + '='.join([k.replace('_', '-'), str(v)]) for k, v in kwargs.items() + ) + + if e.code == EOPNOTSUPP: + raise CoalesceNotSupportedParam( + f'Parameter(s) not supported for modification on ' + f'{self._ifname}: {params_str}' + ) from e + elif e.code == EINVAL: + raise CoalesceInvalidValue( + f'Invalid value for coalesce parameter(s) on ' + f'{self._ifname}: {params_str}' + ) from e + raise + + +def get_coalesce(ifname) -> dict: + """ + Get coalesce parameters for an interface. + + This is a convenience function that creates a CoalesceNetlink instance, + gets the coalesce parameters, and closes the socket. + + Args: + ifname: Interface name (e.g., 'eth0') + + Returns: + dict: Coalesce parameters with None for unsupported values. + """ + with CoalesceNetlink(ifname) as cn: + return cn.get_coalesce() + + +def set_coalesce(ifname, **kwargs): + """ + Set coalesce parameters for an interface. + + This is a convenience function that creates a CoalesceNetlink instance, + sets the coalesce parameters, and closes the socket. + + Args: + ifname: Interface name (e.g., 'eth0') + **kwargs: Coalesce parameters to set. + """ + with CoalesceNetlink(ifname) as cn: + cn.set_coalesce(**kwargs) + + +def get_all_params(boolean: bool = None) -> tuple: + """ + Get all available parameters by the Linux kernel. + + This is a function that gets the coalesce parameters + which are available by implementation and the Linux kernel. + + Args: + boolean: Filter parameters and return only/without boolean types. + + Returns: + tuple: All coalesce parameters + """ + + params = list(COALESCE_NL_ATTRS.keys()) + + def _param_is_bool(p): + nl_type = ethtool_coalesce_msg.get_nl_attr_type(COALESCE_NL_ATTRS[p]) + return nl_type == 'uint8' + + if boolean is not None: + boolean_params = list(filter(_param_is_bool, params)) + + if boolean: + # Use only boolean parameters + params = boolean_params + else: + # Use other parameters except booleans + for boolean_param in boolean_params: + params.remove(boolean_param) + + return tuple(params) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 5b87bd9d1..7c8106521 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -28,6 +28,8 @@ from base_interfaces_test import BasicInterfaceTest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.ethtool import Ethtool +from vyos.netlink import coalesce from vyos.frrender import mgmt_daemon from vyos.ifconfig import Section from vyos.utils.file import read_file @@ -205,6 +207,85 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(max_rx, rx) self.assertEqual(max_tx, tx) + def test_ethtool_coalesce(self): + """ + Verify that coalesce configuration is correctly applied to the interface using netlink + """ + + for interface in self._interfaces: + base_path = self._base_path + [interface, 'interrupt-coalescing'] + ethtool = Ethtool(interface) + is_virtio = ethtool.get_driver_name() == 'virtio_net' + + with self.subTest(interface=interface): + # Verify coalesce support on the NIC before check + supported = ethtool.check_coalesce() + + # If ethtool reports completely unsupported feature, then the CLI commit + # should correctly raise a ConfigSessionError during commit + if not supported: + self.cli_set(base_path + ['rx-usecs', '32']) + self.cli_set(base_path + ['tx-usecs', '32']) + + msg = 'Driver does not fully support coalesce configuration' + with self.assertRaisesRegex(ConfigSessionError, msg): + self.cli_commit() + continue + + # To find out the supported features + supported_rx_usecs = ethtool.check_coalesce('rx_usecs') + supported_tx_usecs = ethtool.check_coalesce('tx_usecs') + supported_adaptive_rx = ethtool.check_coalesce('adaptive_rx') + supported_adaptive_tx = ethtool.check_coalesce('adaptive_tx') + + # Disabled adaptive modes and set custom values + if supported_rx_usecs: + self.cli_set(base_path + ['rx-usecs', '64']) + if supported_tx_usecs: + self.cli_set(base_path + ['tx-usecs', '64']) + + # Force adaptive to be disabled if it is already enabled + params = coalesce.get_coalesce(interface) + if supported_rx_usecs and params['adaptive_rx']: + cmd(f'sudo ethtool --coalesce {interface} adaptive-rx off') + if supported_tx_usecs and params['adaptive_tx']: + cmd(f'sudo ethtool --coalesce {interface} adaptive-tx off') + + # Commit CLI configuration to apply coalescing + self.cli_commit() + + # Query coalesce parameters after applying + params = coalesce.get_coalesce(interface) + + # Assertions: all should reflect configured values + if supported_rx_usecs: + # `virtio-net` doesn't correctly work with this parameter + self.assertEqual(params['rx_usecs'], 0 if is_virtio else 64) + if supported_tx_usecs: + # `virtio-net` doesn't correctly work with this parameter + self.assertEqual(params['tx_usecs'], 0 if is_virtio else 64) + + # Not all parameters are adjustable for some of NIC (`virtio-net`) + if supported_adaptive_rx: + # Now test enabling RX adaptive coalescing modes + self.cli_delete(base_path + ['rx-usecs']) + self.cli_set(base_path + ['adaptive-rx']) + + if supported_adaptive_tx: + # Now test enabling TX adaptive coalescing modes + self.cli_delete(base_path + ['tx-usecs']) + self.cli_set(base_path + ['adaptive-tx']) + + self.cli_commit() + + # Verify that adaptive modes turned on correctly + params = coalesce.get_coalesce(interface) + if supported_adaptive_rx: + self.assertTrue(params['adaptive_rx']) + + if supported_adaptive_tx: + self.assertTrue(params['adaptive_tx']) + def test_ethtool_flow_control(self): for interface in self._interfaces: # Disable flow-control diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 554f0fa41..2a65855a4 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -37,6 +37,7 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member from vyos.configverify import verify_eapol from vyos.ethtool import Ethtool +from vyos.netlink import coalesce from vyos.frrender import FRRender from vyos.frrender import get_frrender_dict from vyos.ifconfig import EthernetIf @@ -256,6 +257,26 @@ def verify_ring_buffer(ethernet: dict, ethtool: Ethtool): f'size of "{max_tx}" bytes!') +def verify_coalesce(ethernet: dict, ethtool: Ethtool): + """ + Verify coalesce settings + :param ethernet: dictionary which is received from get_interface_dict + :type ethernet: dict + :param ethtool: Ethernet object + :type ethtool: Ethtool + """ + if 'interrupt_coalescing' in ethernet: + if not ethtool.check_coalesce(): + raise ConfigError('Driver does not fully support coalesce configuration!') + + for param in coalesce.get_all_params(): + if param in ethernet['interrupt_coalescing']: + if not ethtool.check_coalesce(param): + param_name = param.replace('_', '-') + msg = f'Driver does not support "{param_name}" coalesce setting!' + raise ConfigError(msg) + + def verify_offload(ethernet: dict, ethtool: Ethtool): """ Verify offloading capabilities @@ -398,6 +419,7 @@ def verify(ethernet): verify_ring_buffer(ethernet, ethtool) verify_offload(ethernet, ethtool) verify_mac_change(ethernet, ethtool) + verify_coalesce(ethernet, ethtool) if 'is_bond_member' in ethernet: verify_bond_member(ethernet, ethtool) |
