Skip to content

Commit 8e406f9

Browse files
committed
T7789: T7661: VPP prevent failing to set XDP driver on clouds
1 parent 5bb7816 commit 8e406f9

File tree

5 files changed

+141
-20
lines changed

5 files changed

+141
-20
lines changed

python/vyos/ethtool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Ethtool:
5959
_ring_buffer = None
6060
_driver_name = None
6161
_flow_control = None
62+
_channels = ''
6263

6364
def __init__(self, ifname):
6465
# Get driver used for interface
@@ -111,6 +112,11 @@ def __init__(self, ifname):
111112
if not bool(err):
112113
self._flow_control = loads(out)[0]
113114

115+
# Get information about NIC channels
116+
out, err = popen(f'ethtool --show-channels {ifname}')
117+
if not bool(err):
118+
self._channels = out.lower()
119+
114120
def check_auto_negotiation_supported(self):
115121
""" Check if the NIC supports changing auto-negotiation """
116122
return self._base_settings['supports-auto-negotiation']
@@ -199,3 +205,12 @@ def get_flow_control(self):
199205
'flow-control settings!')
200206

201207
return 'on' if bool(self._flow_control['autonegotiate']) else 'off'
208+
209+
def get_channel(self, rx_tx_comb):
210+
# Configuration of RX/TX or Combined channels is not supported on every device,
211+
# thus when it's impossible return None
212+
if rx_tx_comb not in ['rx', 'tx', 'combined']:
213+
raise ValueError('Channel type must be either "rx", "tx" or "combined"')
214+
match = re.search(rf'{rx_tx_comb}:\s+(\d+)', self._channels)
215+
return int(match.group(1)) if match else None
216+

python/vyos/ifconfig/ethernet.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,23 @@ def set_ring_buffer(self, rx_tx, size):
455455
print(f'could not set "{rx_tx}" ring-buffer for {ifname}')
456456
return output
457457

458+
def set_channel(self, channel, queues):
459+
"""
460+
Example:
461+
>>> from vyos.ifconfig import EthernetIf
462+
>>> i = EthernetIf('eth0')
463+
>>> i.set_channels('rx', 2)
464+
"""
465+
ifname = self.config['ifname']
466+
cmd = f'ethtool --set-channels {ifname} {channel} {queues}'
467+
output, code = self._popen(cmd)
468+
# ethtool error codes:
469+
# 80 - value already setted
470+
# 81 - does not possible to set value
471+
if code and code != 80:
472+
print(f'could not set "{channel}" channel for {ifname}')
473+
return output
474+
458475
def set_switchdev(self, enable):
459476
ifname = self.config['ifname']
460477
addr, code = self._popen(

python/vyos/vpp/control_host.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
from pyroute2 import IPRoute
2525

26+
from vyos.ethtool import Ethtool
27+
from vyos.ifconfig import EthernetIf
2628
from vyos.vpp.utils import EthtoolGDrvinfo
2729

2830

@@ -373,3 +375,38 @@ def flush_ip(iface_name: str) -> None:
373375
"""
374376
iproute = IPRoute()
375377
iproute.flush_addr(label=iface_name)
378+
379+
380+
def get_eth_channels(iface_name: str) -> dict:
381+
"""
382+
Get channel configuration of a network interface using ethtool.
383+
384+
Args:
385+
iface_name (str): name of an interface
386+
387+
Returns:
388+
dict: Dictionary containing the number of channels with keys:
389+
- 'rx' (int | None): RX channel count.
390+
- 'tx' (int | None): TX channel count.
391+
- 'combined' (int | None): Combined channel count.
392+
"""
393+
ethtool = Ethtool(iface_name)
394+
395+
channels = {}
396+
for channel in ['rx', 'tx', 'combined']:
397+
channels[channel] = ethtool.get_channel(channel)
398+
399+
return channels
400+
401+
402+
def set_eth_channels(iface_name: str, channels: dict) -> None:
403+
"""Configure the number of RX, TX, or combined channels for an interface.
404+
405+
Args:
406+
iface_name (str): name of an interface
407+
channels (dict): channels to set
408+
"""
409+
interface = EthernetIf(iface_name)
410+
for channel, value in channels.items():
411+
if value:
412+
interface.set_channel(channel, value)

python/vyos/vpp/control_vpp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ def xdp_iface_create(
351351
self,
352352
host_if: str,
353353
name: str,
354-
rxq_num: int = 0,
354+
rxq_num: int = 65535,
355355
rxq_size: int = 0,
356356
txq_size: int = 0,
357357
mode: Literal['auto', 'copy', 'zero-copy'] = 'auto',

src/conf_mode/vpp.py

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
'virtio_net': ['xdp'],
113113
}
114114

115+
# drivers that does not support changing channels
116+
not_ethtool_channel_change_drv: list[str] = ['vmxnet3', 'virtio_net', 'tun', 'tap']
117+
115118

116119
def _load_module(module_name: str):
117120
"""
@@ -171,6 +174,25 @@ def _normalize_buffers(config: dict):
171174
config['settings']['buffers']['buffers_per_numa'] = str(buffers)
172175

173176

177+
def _get_max_xdp_rx_queues(config: dict):
178+
"""
179+
Count max number of RX queues for XDP driver
180+
- Only half of the available queues are used (to avoid NIC issues).
181+
- If the interface driver is in `not_ethtool_channel_change_drv`, the full
182+
number of queues is returned.
183+
- If neither `rx` nor `combined` is set, return 1.
184+
"""
185+
for key in ('rx', 'combined'):
186+
value = config['channels'].get(key)
187+
if value is not None:
188+
if config['original_driver'] not in not_ethtool_channel_change_drv:
189+
return max(1, int(value) // 2)
190+
else:
191+
return int(value)
192+
193+
return 1
194+
195+
174196
def get_config(config=None):
175197
# use persistent config to store interfaces data between executions
176198
# this is required because some interfaces after they are connected
@@ -284,6 +306,20 @@ def get_config(config=None):
284306
_normalize_buffers(effective_config)
285307
config['effective'] = effective_config
286308

309+
# Save important info about all interfaces that cannot be retrieved later
310+
# Add new interfaces (only if they are first time seen in a config)
311+
for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
312+
if iface not in effective_config.get('settings', {}).get('interface', {}):
313+
eth_ifaces_persist[iface] = {
314+
'original_driver': EthtoolGDrvinfo(iface).driver,
315+
}
316+
eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface)
317+
eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface)
318+
eth_ifaces_persist[iface]['channels'] = control_host.get_eth_channels(iface)
319+
320+
# Return to config dictionary
321+
config['persist_config'] = eth_ifaces_persist
322+
287323
if 'settings' in config:
288324
if 'interface' in config['settings']:
289325
for iface, iface_config in config['settings']['interface'].items():
@@ -307,11 +343,11 @@ def get_config(config=None):
307343
iface_filter_eth(conf, iface)
308344
set_dependents('ethernet', conf, iface)
309345
# Interfaces with changed driver should be removed/readded
310-
if old_driver and old_driver[0] == 'dpdk':
346+
if old_driver:
311347
removed_ifaces.append(
312348
{
313349
'iface_name': iface,
314-
'driver': 'dpdk',
350+
'driver': old_driver[0],
315351
}
316352
)
317353

@@ -344,7 +380,8 @@ def get_config(config=None):
344380
'txq_size': int(iface_config['xdp_options']['tx_queue_size']),
345381
}
346382
if iface_config['xdp_options']['num_rx_queues'] == 'all':
347-
xdp_api_params['rxq_num'] = 0
383+
# 65535 is used as special value to request all available queues
384+
xdp_api_params['rxq_num'] = 65535
348385
else:
349386
xdp_api_params['rxq_num'] = int(
350387
iface_config['xdp_options']['num_rx_queues']
@@ -374,26 +411,11 @@ def get_config(config=None):
374411
iface_filter_eth(conf, iface)
375412
set_dependents(dependency, conf, iface)
376413

377-
# Save important info about all interfaces that cannot be retrieved later
378-
# Add new interfaces (only if they are first time seen in a config)
379-
for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
380-
if iface not in effective_config.get('settings', {}).get('interface', {}):
381-
eth_ifaces_persist[iface] = {
382-
'original_driver': config['settings']['interface'][iface][
383-
'kernel_module'
384-
],
385-
}
386-
eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface)
387-
eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface)
388-
389414
# PPPoE dependency
390415
if pppoe_map_ifaces:
391416
config['pppoe_ifaces'] = pppoe_map_ifaces
392417
set_dependents('pppoe_server', conf)
393418

394-
# Return to config dictionary
395-
config['persist_config'] = eth_ifaces_persist
396-
397419
return config
398420

399421

@@ -485,6 +507,14 @@ def verify(config):
485507
)
486508
if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config:
487509
if iface_config['xdp_options']['num_rx_queues'] != 'all':
510+
rx_queues = iface_config['xdp_api_params']['rxq_num']
511+
max_rx_queues = _get_max_xdp_rx_queues(config['persist_config'][iface])
512+
if rx_queues > max_rx_queues:
513+
raise ConfigError(
514+
f'Maximum supported number of RX queues for interface {iface} is {max_rx_queues}. '
515+
f'Please set "xdp-options num-rx-queues" to {max_rx_queues} or fewer'
516+
)
517+
488518
Warning(f'Not all RX queues will be connected to VPP for {iface}!')
489519

490520
if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config:
@@ -593,8 +623,10 @@ def initialize_interface(iface, driver, iface_config) -> None:
593623
iface_new_name: str = control_host.get_eth_name(iface_config['dev_id'])
594624
control_host.rename_iface(iface_new_name, iface)
595625

596-
# XDP - rename an interface, disable promisc and XDP
626+
# XDP - rename an interface, set original channels, disable promisc and XDP
597627
if driver == 'xdp':
628+
if iface_config['original_driver'] not in not_ethtool_channel_change_drv:
629+
control_host.set_eth_channels(f'defunct_{iface}', iface_config['channels'])
598630
control_host.set_promisc(f'defunct_{iface}', 'off')
599631
control_host.rename_iface(f'defunct_{iface}', iface)
600632
control_host.xdp_remove(iface)
@@ -691,6 +723,26 @@ def apply(config):
691723
# add XDP interfaces
692724
if iface_config['driver'] == 'xdp':
693725
control_host.rename_iface(iface, f'defunct_{iface}')
726+
727+
# Some NICs fail to load XDP if all RX queues are configured. To avoid this,
728+
# we limit the number of queues to half of the maximum supported by the driver.
729+
# If the NIC driver does not support ethtool channel changes, no adjustment is made.
730+
if (
731+
config['persist_config'][iface]['original_driver']
732+
not in not_ethtool_channel_change_drv
733+
):
734+
max_rx_queues = _get_max_xdp_rx_queues(
735+
config['persist_config'][iface]
736+
)
737+
channels_orig = config['persist_config'][iface]['channels']
738+
channels = {}
739+
if channels_orig.get('rx'):
740+
channels = {'rx': max_rx_queues, 'tx': max_rx_queues}
741+
if channels_orig.get('combined'):
742+
channels['combined'] = max_rx_queues
743+
if channels:
744+
control_host.set_eth_channels(f'defunct_{iface}', channels)
745+
694746
vpp_control.xdp_iface_create(
695747
host_if=f'defunct_{iface}',
696748
name=iface,

0 commit comments

Comments
 (0)