From 3d5cc842ea651de68cfac299d6a8bd2849c9a1f8 Mon Sep 17 00:00:00 2001 From: Nicolas Vollmar Date: Thu, 12 Dec 2024 19:34:20 +0100 Subject: [PATCH 1/2] T6944: adds option to enable switchdev mode on ethernet interface --- .../interfaces_ethernet.xml.in | 6 ++++++ python/vyos/ifconfig/ethernet.py | 21 +++++++++++++++++++ .../scripts/cli/test_interfaces_ethernet.py | 9 ++++++++ 3 files changed, 36 insertions(+) diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in index 89f990d411..b3559a6261 100644 --- a/interface-definitions/interfaces_ethernet.xml.in +++ b/interface-definitions/interfaces_ethernet.xml.in @@ -56,6 +56,12 @@ auto + + + Enables switchdev mode on interface + + + #include diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 50dd0f3961..a8b72cc9ce 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -417,6 +417,25 @@ def set_ring_buffer(self, rx_tx, size): 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 get_config_dict(). It's main intention is to consolidate the scattered @@ -463,6 +482,8 @@ def update(self, config): 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 c02ca613be..eeebaa4ca5 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -223,5 +223,14 @@ def test_ethtool_evpn_uplink_tarcking(self): frrconfig = self.getFRRconfig(f'interface {interface}', daemon=mgmt_daemon) self.assertIn(f' 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) From 9d53f47a305a39caf15c73cacaf29177cebbaf14 Mon Sep 17 00:00:00 2001 From: Nicolas Vollmar Date: Thu, 19 Dec 2024 11:13:36 +0100 Subject: [PATCH 2/2] T6944: fix unrelated formatting --- python/vyos/ifconfig/ethernet.py | 158 +++++++++++------- .../scripts/cli/test_interfaces_ethernet.py | 34 ++-- 2 files changed, 123 insertions(+), 69 deletions(-) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index a8b72cc9ce..d0c03dbe0e 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -26,11 +26,13 @@ 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 @@ def feature(ifname, option, value): 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 @@ def get_bond_member_allowed_options() -> list: 'ring_buffer.rx', 'ring_buffer.tx', 'speed', - 'hw_id' + 'hw_id', ] return bond_allowed_sections @@ -130,7 +135,11 @@ def remove(self): 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 @@ def set_flow_control(self, enable): 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 @@ def set_speed_duplex(self, speed, duplex): """ 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 @@ def set_speed_duplex(self, speed, duplex): # 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 @@ def set_gro(self, state): 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 @@ def set_gso(self, state): 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 @@ def set_lro(self, state): 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 @@ def set_rps(self, state): 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 @@ def set_rfs(self, state): 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 @@ def set_tso(self, state): 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): @@ -419,56 +457,62 @@ def set_ring_buffer(self, rx_tx, size): def set_switchdev(self, enable): ifname = self.config['ifname'] - addr, code = self._popen(f'ethtool -i {ifname} | grep bus-info | awk \'{{print $2}}\'') + 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}}\'') + 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') + 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: diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index eeebaa4ca5..183c10250d 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -28,10 +28,12 @@ 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 @@ def tearDown(self): 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 @@ def test_offloading_rps(self): 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 @@ def test_offloading_rfs(self): 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 @@ def test_offloading_rfs(self): 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,7 @@ def test_ethtool_evpn_uplink_tarcking(self): 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] @@ -232,5 +241,6 @@ def test_switchdev(self): self.cli_delete(self._base_path + [interface, 'switchdev']) + if __name__ == '__main__': unittest.main(verbosity=2)