diff options
| author | Nataliia S. <81954790+natali-rs1985@users.noreply.github.com> | 2025-11-04 15:15:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-04 15:15:59 +0200 |
| commit | 4f9c241db48928f974a32bc6901b9ed4cf2ce20e (patch) | |
| tree | d781bbe53e9769cf669d8d792b5c0519e67a58c2 | |
| parent | e1b7b865a63454ae4b8f840c94ab599633faaf7f (diff) | |
| parent | 73b7431862d8da2e8063a6062f29fd0f65fdd210 (diff) | |
| download | vyos-1x-4f9c241db48928f974a32bc6901b9ed4cf2ce20e.tar.gz vyos-1x-4f9c241db48928f974a32bc6901b9ed4cf2ce20e.zip | |
Merge pull request #4747 from natali-rs1985/T7789
T7789: T7661: VPP prevent failing to load XDP in clouds (ena/gve drivers)
| -rw-r--r-- | python/vyos/ethtool.py | 23 | ||||
| -rw-r--r-- | python/vyos/ifconfig/ethernet.py | 17 | ||||
| -rw-r--r-- | python/vyos/vpp/control_host.py | 38 | ||||
| -rw-r--r-- | python/vyos/vpp/control_vpp.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 85 |
5 files changed, 147 insertions, 18 deletions
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 6c362163c..22e066def 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -59,6 +59,7 @@ class Ethtool: _ring_buffer = None _driver_name = None _flow_control = None + _channels = '' def __init__(self, ifname): # Get driver used for interface @@ -111,6 +112,11 @@ class Ethtool: if not bool(err): self._flow_control = loads(out)[0] + # Get information about NIC channels + out, err = popen(f'ethtool --show-channels {ifname}') + if not bool(err): + self._channels = out.lower() + def check_auto_negotiation_supported(self): """ Check if the NIC supports changing auto-negotiation """ return self._base_settings['supports-auto-negotiation'] @@ -199,3 +205,20 @@ class Ethtool: 'flow-control settings!') return 'on' if bool(self._flow_control['autonegotiate']) else 'off' + + def get_channels(self, rx_tx_comb): + """ + Get both the pre-set maximum and current value for a given channel type. + + Args: + rx_tx_comb (str): Channel type, one of "rx", "tx", or "combined". + + Returns: + list[int]: [maximum, current] values for the channel, + or an empty list if not supported. + """ + if rx_tx_comb not in ['rx', 'tx', 'combined']: + raise ValueError('Channel type must be either "rx", "tx" or "combined"') + matches = re.findall(rf'{rx_tx_comb}:\s+(\d+)', self._channels) + + return [int(value) for value in matches] diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 6c7b230ca..70da837b9 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -455,6 +455,23 @@ class EthernetIf(Interface): print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output + def set_channels(self, rx_tx_comb, queues): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_channels('rx', 2) + """ + ifname = self.config['ifname'] + cmd = f'ethtool --set-channels {ifname} {rx_tx_comb} {queues}' + output, code = self._popen(cmd) + # ethtool error codes: + # 80 - value already setted + # 81 - does not possible to set value + if code and code != 80: + print(f'could not set "{rx_tx_comb}" channel for {ifname}') + return output + def set_switchdev(self, enable): ifname = self.config['ifname'] addr, code = self._popen( diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py index db1a492c9..2ebfd180b 100644 --- a/python/vyos/vpp/control_host.py +++ b/python/vyos/vpp/control_host.py @@ -24,6 +24,7 @@ from time import sleep from pyroute2 import IPRoute from vyos.ethtool import Ethtool +from vyos.ifconfig import EthernetIf from vyos.vpp.utils import EthtoolGDrvinfo @@ -376,3 +377,40 @@ def flush_ip(iface_name: str) -> None: """ iproute = IPRoute() iproute.flush_addr(label=iface_name) + + +def get_eth_channels(iface_name: str) -> dict: + """ + Get the current hardware queue counts for channels of an interface using ethtool. + + Args: + iface_name (str): name of an interface + + Returns: + dict: Mapping of channel types to their current values: + - 'rx' (int | None): RX channel count. + - 'tx' (int | None): TX channel count. + - 'combined' (int | None): Combined channel count. + Returns None if the channel type is not supported. + """ + ethtool = Ethtool(iface_name) + + channels = {} + for channel in ['rx', 'tx', 'combined']: + queues_list = ethtool.get_channels(channel) + channels[channel] = queues_list[-1] if (len(queues_list) == 2) else None + + return channels + + +def set_eth_channels(iface_name: str, channels: dict) -> None: + """Configure the number of RX, TX, or combined channels for an interface. + + Args: + iface_name (str): name of an interface + channels (dict): channels to set + """ + interface = EthernetIf(iface_name) + for channel, value in channels.items(): + if value: + interface.set_channels(channel, value) diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index cb8da50a6..2ee253b45 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -351,7 +351,7 @@ class VPPControl: self, host_if: str, name: str, - rxq_num: int = 0, + rxq_num: int = 65535, rxq_size: int = 0, txq_size: int = 0, mode: Literal['auto', 'copy', 'zero-copy'] = 'auto', diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 61b10406f..bd7ff173f 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -112,6 +112,9 @@ drivers_support_interrupt: dict[str, list] = { 'virtio_net': ['xdp'], } +# drivers that require changing channels (half the maximum number of RX/TX queues) +ethtool_channels_change_drv: list[str] = ['ena', 'gve'] + def _load_module(module_name: str): """ @@ -171,6 +174,25 @@ def _normalize_buffers(config: dict): config['settings']['buffers']['buffers_per_numa'] = str(buffers) +def _get_max_xdp_rx_queues(config: dict): + """ + Count max number of RX queues for XDP driver + - If the interface driver is in `ethtool_channels_change_drv` + only half of the available queues are used (to avoid NIC issues) + - For other interface drivers the full number of queues is returned. + - If neither `rx` nor `combined` is set, return 1. + """ + for key in ('rx', 'combined'): + value = config['channels'].get(key) + if value: + if config['original_driver'] in ethtool_channels_change_drv: + return max(1, int(value) // 2) + else: + return int(value) + + return 1 + + def get_config(config=None): # use persistent config to store interfaces data between executions # this is required because some interfaces after they are connected @@ -270,6 +292,20 @@ def get_config(config=None): _normalize_buffers(effective_config) config['effective'] = effective_config + # Save important info about all interfaces that cannot be retrieved later + # Add new interfaces (only if they are first time seen in a config) + for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): + if iface not in effective_config.get('settings', {}).get('interface', {}): + eth_ifaces_persist[iface] = { + 'original_driver': EthtoolGDrvinfo(iface).driver, + } + eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) + eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + eth_ifaces_persist[iface]['channels'] = control_host.get_eth_channels(iface) + + # Return to config dictionary + config['persist_config'] = eth_ifaces_persist + if 'settings' in config: if 'interface' in config['settings']: for iface, iface_config in config['settings']['interface'].items(): @@ -330,7 +366,8 @@ def get_config(config=None): 'txq_size': int(iface_config['xdp_options']['tx_queue_size']), } if iface_config['xdp_options']['num_rx_queues'] == 'all': - xdp_api_params['rxq_num'] = 0 + # 65535 is used as special value to request all available queues + xdp_api_params['rxq_num'] = 65535 else: xdp_api_params['rxq_num'] = int( iface_config['xdp_options']['num_rx_queues'] @@ -363,18 +400,6 @@ def get_config(config=None): iface_filter_eth(conf, iface) set_dependents(dependency, conf, iface) - # Save important info about all interfaces that cannot be retrieved later - # Add new interfaces (only if they are first time seen in a config) - for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): - if iface not in effective_config.get('settings', {}).get('interface', {}): - eth_ifaces_persist[iface] = { - 'original_driver': config['settings']['interface'][iface][ - 'kernel_module' - ], - } - eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) - eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) - # kernel-interfaces dependency if effective_config.get('kernel_interfaces'): for iface in config.get('kernel_interfaces', {}): @@ -399,9 +424,6 @@ def get_config(config=None): config['pppoe_ifaces'] = pppoe_map_ifaces set_dependents('pppoe_server', conf) - # Return to config dictionary - config['persist_config'] = eth_ifaces_persist - return config @@ -493,6 +515,14 @@ def verify(config): ) if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config: if iface_config['xdp_options']['num_rx_queues'] != 'all': + rx_queues = iface_config['xdp_api_params']['rxq_num'] + max_rx_queues = _get_max_xdp_rx_queues(config['persist_config'][iface]) + if rx_queues > max_rx_queues: + raise ConfigError( + f'Maximum supported number of RX queues for interface {iface} is {max_rx_queues}. ' + f'Please set "xdp-options num-rx-queues" to {max_rx_queues} or fewer' + ) + Warning(f'Not all RX queues will be connected to VPP for {iface}!') if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config: @@ -601,11 +631,13 @@ def initialize_interface(iface, driver, iface_config) -> None: iface_new_name: str = control_host.get_eth_name(iface_config['dev_id']) control_host.rename_iface(iface_new_name, iface) - # XDP - rename an interface, disable promisc and XDP + # XDP - rename an interface, disable promisc and XDP, set original channels if driver == 'xdp': control_host.set_promisc(f'defunct_{iface}', 'off') control_host.rename_iface(f'defunct_{iface}', iface) control_host.xdp_remove(iface) + if iface_config['original_driver'] in ethtool_channels_change_drv: + control_host.set_eth_channels(iface, iface_config['channels']) # Rename Mellanox NIC to a normal name try: @@ -720,6 +752,25 @@ def apply(config): # add XDP interfaces if iface_config['driver'] == 'xdp': control_host.rename_iface(iface, f'defunct_{iface}') + + # Some cloud NICs fail to load XDP if all RX queues are configured. To avoid this, + # we limit the number of queues to half of the maximum supported by the driver. + if ( + config['persist_config'][iface]['original_driver'] + in ethtool_channels_change_drv + ): + max_rx_queues = _get_max_xdp_rx_queues( + config['persist_config'][iface] + ) + channels_orig = config['persist_config'][iface]['channels'] + channels = {} + if channels_orig.get('rx'): + channels = {'rx': max_rx_queues, 'tx': max_rx_queues} + if channels_orig.get('combined'): + channels['combined'] = max_rx_queues + if channels: + control_host.set_eth_channels(f'defunct_{iface}', channels) + vpp_control.xdp_iface_create( host_if=f'defunct_{iface}', name=iface, |
