From f01ff15b7530053e18bb79cd529a84e3f15b294a Mon Sep 17 00:00:00 2001 From: Oleksandr Kuchmystyi Date: Thu, 25 Dec 2025 16:48:03 +0300 Subject: ethtool: T7730: Add configuration coalesce for interface This change introduces CLI support for configuring network interface interrupt coalescing parameters via `netlink`. Note: Not all NIC drivers support interrupt coalescing. On unsupported interfaces, `netlink` returns an error and the VyOS commit will fail. --- python/vyos/ethtool.py | 20 +++ python/vyos/ifconfig/ethernet.py | 56 ++++++ python/vyos/netlink/__init__.py | 14 ++ python/vyos/netlink/coalesce.py | 363 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 453 insertions(+) create mode 100644 python/vyos/netlink/__init__.py create mode 100644 python/vyos/netlink/coalesce.py (limited to 'python') 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 . 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 +# +# 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 . 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 +# +# 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 . + + +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) -- cgit v1.2.3