diff options
author | Christian Breunig <christian@breunig.cc> | 2024-12-24 14:23:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-24 14:23:20 +0100 |
commit | 77d5b096bb91964b7abc9fc495f574333184edbf (patch) | |
tree | 3e0e92fcb66c1b1dadf25a544273769569966829 | |
parent | b05cbfa71570bce1f4f470551e5c0217d14ba5a8 (diff) | |
parent | 9d53f47a305a39caf15c73cacaf29177cebbaf14 (diff) | |
download | vyos-1x-77d5b096bb91964b7abc9fc495f574333184edbf.tar.gz vyos-1x-77d5b096bb91964b7abc9fc495f574333184edbf.zip |
Merge pull request #4235 from nvollmar/T6944
T6944: adds option to enable switchdev mode on ethernet interface
-rw-r--r-- | interface-definitions/interfaces_ethernet.xml.in | 6 | ||||
-rw-r--r-- | python/vyos/ifconfig/ethernet.py | 173 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 43 |
3 files changed, 156 insertions, 66 deletions
diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in index 89f990d41..b3559a626 100644 --- a/interface-definitions/interfaces_ethernet.xml.in +++ b/interface-definitions/interfaces_ethernet.xml.in @@ -56,6 +56,12 @@ </properties> <defaultValue>auto</defaultValue> </leafNode> + <leafNode name="switchdev"> + <properties> + <help>Enables switchdev mode on interface</help> + <valueless/> + </properties> + </leafNode> #include <include/interface/eapol.xml.i> <node name="evpn"> <properties> diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 50dd0f396..d0c03dbe0 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -26,11 +26,13 @@ from vyos.utils.file import read_file from vyos.utils.process import run from vyos.utils.assertion import assert_list + @Interface.register class EthernetIf(Interface): """ Abstraction of a Linux Ethernet Interface """ + iftype = 'ethernet' definition = { **Interface.definition, @@ -41,7 +43,7 @@ class EthernetIf(Interface): 'broadcast': True, 'bridgeable': True, 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$', - } + }, } @staticmethod @@ -49,32 +51,35 @@ class EthernetIf(Interface): run(f'ethtool --features {ifname} {option} {value}') return False - _command_set = {**Interface._command_set, **{ - 'gro': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v), - }, - 'gso': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v), - }, - 'hw-tc-offload': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'hw-tc-offload', v), - }, - 'lro': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'lro', v), - }, - 'sg': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v), - }, - 'tso': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), + _command_set = { + **Interface._command_set, + **{ + 'gro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v), + }, + 'gso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v), + }, + 'hw-tc-offload': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'hw-tc-offload', v), + }, + 'lro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'lro', v), + }, + 'sg': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v), + }, + 'tso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), + }, }, - }} + } @staticmethod def get_bond_member_allowed_options() -> list: @@ -106,7 +111,7 @@ class EthernetIf(Interface): 'ring_buffer.rx', 'ring_buffer.tx', 'speed', - 'hw_id' + 'hw_id', ] return bond_allowed_sections @@ -130,7 +135,11 @@ class EthernetIf(Interface): self.set_admin_state('down') # Remove all VLAN subinterfaces - filter with the VLAN dot - for vlan in [x for x in Section.interfaces(self.iftype) if x.startswith(f'{self.ifname}.')]: + for vlan in [ + x + for x in Section.interfaces(self.iftype) + if x.startswith(f'{self.ifname}.') + ]: Interface(vlan).remove() super().remove() @@ -149,10 +158,12 @@ class EthernetIf(Interface): ifname = self.config['ifname'] if enable not in ['on', 'off']: - raise ValueError("Value out of range") + raise ValueError('Value out of range') if not self.ethtool.check_flow_control(): - self._debug_msg(f'NIC driver does not support changing flow control settings!') + self._debug_msg( + 'NIC driver does not support changing flow control settings!' + ) return False current = self.ethtool.get_flow_control() @@ -180,12 +191,24 @@ class EthernetIf(Interface): """ ifname = self.config['ifname'] - if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', - '25000', '40000', '50000', '100000', '400000']: - raise ValueError("Value out of range (speed)") + if speed not in [ + 'auto', + '10', + '100', + '1000', + '2500', + '5000', + '10000', + '25000', + '40000', + '50000', + '100000', + '400000', + ]: + raise ValueError('Value out of range (speed)') if duplex not in ['auto', 'full', 'half']: - raise ValueError("Value out of range (duplex)") + raise ValueError('Value out of range (duplex)') if not self.ethtool.check_speed_duplex(speed, duplex): Warning(f'changing speed/duplex setting on "{ifname}" is unsupported!') @@ -224,7 +247,9 @@ class EthernetIf(Interface): # but they do not actually support it either. # In that case it's probably better to ignore the error # than end up with a broken config. - print('Warning: could not set speed/duplex settings: operation not permitted!') + print( + 'Warning: could not set speed/duplex settings: operation not permitted!' + ) def set_gro(self, state): """ @@ -243,7 +268,9 @@ class EthernetIf(Interface): if not fixed: return self.set_interface('gro', 'on' if state else 'off') else: - print('Adapter does not support changing generic-receive-offload settings!') + print( + 'Adapter does not support changing generic-receive-offload settings!' + ) return False def set_gso(self, state): @@ -262,7 +289,9 @@ class EthernetIf(Interface): if not fixed: return self.set_interface('gso', 'on' if state else 'off') else: - print('Adapter does not support changing generic-segmentation-offload settings!') + print( + 'Adapter does not support changing generic-segmentation-offload settings!' + ) return False def set_hw_tc_offload(self, state): @@ -300,7 +329,9 @@ class EthernetIf(Interface): if not fixed: return self.set_interface('lro', 'on' if state else 'off') else: - print('Adapter does not support changing large-receive-offload settings!') + print( + 'Adapter does not support changing large-receive-offload settings!' + ) return False def set_rps(self, state): @@ -332,13 +363,15 @@ class EthernetIf(Interface): for i in range(0, cpu_count, 32): # Extract the next 32-bit chunk chunk = (rps_cpus >> i) & 0xFFFFFFFF - hex_chunks.append(f"{chunk:08x}") + hex_chunks.append(f'{chunk:08x}') # Join the chunks with commas - rps_cpus = ",".join(hex_chunks) + rps_cpus = ','.join(hex_chunks) for i in range(queues): - self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', rps_cpus) + self._write_sysfs( + f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', rps_cpus + ) # send bitmask representation as hex string without leading '0x' return True @@ -348,10 +381,13 @@ class EthernetIf(Interface): queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*')) if state: global_rfs_flow = 32768 - rfs_flow = int(global_rfs_flow/queues) + rfs_flow = int(global_rfs_flow / queues) for i in range(0, queues): - self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_flow_cnt', rfs_flow) + self._write_sysfs( + f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_flow_cnt', + rfs_flow, + ) return True @@ -392,7 +428,9 @@ class EthernetIf(Interface): if not fixed: return self.set_interface('tso', 'on' if state else 'off') else: - print('Adapter does not support changing tcp-segmentation-offload settings!') + print( + 'Adapter does not support changing tcp-segmentation-offload settings!' + ) return False def set_ring_buffer(self, rx_tx, size): @@ -417,39 +455,64 @@ class EthernetIf(Interface): print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output + def set_switchdev(self, enable): + ifname = self.config['ifname'] + addr, code = self._popen( + f"ethtool -i {ifname} | grep bus-info | awk '{{print $2}}'" + ) + if code != 0: + print(f'could not resolve PCIe address of {ifname}') + return + + enabled = False + state, code = self._popen( + f"/sbin/devlink dev eswitch show pci/{addr} | awk '{{print $3}}'" + ) + if code == 0 and state == 'switchdev': + enabled = True + + if enable and not enabled: + output, code = self._popen( + f'/sbin/devlink dev eswitch set pci/{addr} mode switchdev' + ) + if code != 0: + print(f'{ifname} does not support switchdev mode') + elif not enable and enabled: + self._cmd(f'/sbin/devlink dev eswitch set pci/{addr} mode legacy') + def update(self, config): - """ General helper function which works on a dictionary retrived by + """General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin - on any interface. """ + on any interface.""" # disable ethernet flow control (pause frames) value = 'off' if 'disable_flow_control' in config else 'on' self.set_flow_control(value) # GRO (generic receive offload) - self.set_gro(dict_search('offload.gro', config) != None) + self.set_gro(dict_search('offload.gro', config) is not None) # GSO (generic segmentation offload) - self.set_gso(dict_search('offload.gso', config) != None) + self.set_gso(dict_search('offload.gso', config) is not None) # GSO (generic segmentation offload) - self.set_hw_tc_offload(dict_search('offload.hw_tc_offload', config) != None) + self.set_hw_tc_offload(dict_search('offload.hw_tc_offload', config) is not None) # LRO (large receive offload) - self.set_lro(dict_search('offload.lro', config) != None) + self.set_lro(dict_search('offload.lro', config) is not None) # RPS - Receive Packet Steering - self.set_rps(dict_search('offload.rps', config) != None) + self.set_rps(dict_search('offload.rps', config) is not None) # RFS - Receive Flow Steering - self.set_rfs(dict_search('offload.rfs', config) != None) + self.set_rfs(dict_search('offload.rfs', config) is not None) # scatter-gather option - self.set_sg(dict_search('offload.sg', config) != None) + self.set_sg(dict_search('offload.sg', config) is not None) # TSO (TCP segmentation offloading) - self.set_tso(dict_search('offload.tso', config) != None) + self.set_tso(dict_search('offload.tso', config) is not None) # Set physical interface speed and duplex if 'speed_duplex_changed' in config: @@ -463,6 +526,8 @@ class EthernetIf(Interface): for rx_tx, size in config['ring_buffer'].items(): self.set_ring_buffer(rx_tx, size) + self.set_switchdev('switchdev' in config) + # call base class last super().update(config) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index c02ca613b..183c10250 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -28,10 +28,12 @@ from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.frrender import mgmt_daemon -from vyos.utils.process import cmd -from vyos.utils.process import popen from vyos.utils.file import read_file +from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local +from vyos.utils.process import cmd +from vyos.utils.process import popen + class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -78,14 +80,18 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): continue self.assertFalse(is_intf_addr_assigned(interface, addr['addr'])) # Ensure no VLAN interfaces are left behind - tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')] + tmp = [ + x + for x in Section.interfaces('ethernet') + if x.startswith(f'{interface}.') + ] self.assertListEqual(tmp, []) def test_offloading_rps(self): # enable RPS on all available CPUs, RPS works with a CPU bitmask, # where each bit represents a CPU (core/thread). The formula below # expands to rps_cpus = 255 for a 8 core system - rps_cpus = (1 << os.cpu_count()) -1 + rps_cpus = (1 << os.cpu_count()) - 1 # XXX: we should probably reserve one core when the system is under # high preasure so we can still have a core left for housekeeping. @@ -101,7 +107,7 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: cpus = read_file(f'/sys/class/net/{interface}/queues/rx-0/rps_cpus') # remove the nasty ',' separation on larger strings - cpus = cpus.replace(',','') + cpus = cpus.replace(',', '') cpus = int(cpus, 16) self.assertEqual(f'{cpus:x}', f'{rps_cpus:x}') @@ -117,12 +123,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*')) - rfs_flow = int(global_rfs_flow/queues) + rfs_flow = int(global_rfs_flow / queues) for i in range(0, queues): - tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt') + tmp = read_file( + f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt' + ) self.assertEqual(int(tmp), rfs_flow) - tmp = read_file(f'/proc/sys/net/core/rps_sock_flow_entries') + tmp = read_file('/proc/sys/net/core/rps_sock_flow_entries') self.assertEqual(int(tmp), global_rfs_flow) # delete configuration of RFS and check all values returned to default "0" @@ -133,12 +141,13 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*')) - rfs_flow = int(global_rfs_flow/queues) + rfs_flow = int(global_rfs_flow / queues) for i in range(0, queues): - tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt') + tmp = read_file( + f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt' + ) self.assertEqual(int(tmp), 0) - def test_non_existing_interface(self): unknonw_interface = self._base_path + ['eth667'] self.cli_set(unknonw_interface) @@ -221,7 +230,17 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: frrconfig = self.getFRRconfig(f'interface {interface}', daemon=mgmt_daemon) - self.assertIn(f' evpn mh uplink', frrconfig) + self.assertIn(' evpn mh uplink', frrconfig) + + def test_switchdev(self): + interface = self._interfaces[0] + self.cli_set(self._base_path + [interface, 'switchdev']) + + # check validate() - virtual interfaces do not support switchdev + # should print out warning that enabling failed + + self.cli_delete(self._base_path + [interface, 'switchdev']) + if __name__ == '__main__': unittest.main(verbosity=2) |