# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io> # # 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 <http://www.gnu.org/licenses/>. import os from glob import glob from vyos.base import Warning from vyos.ethtool import Ethtool from vyos.ifconfig import Section from vyos.ifconfig.interface import Interface from vyos.utils.dict import dict_search 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, **{ 'section': 'ethernet', 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'], 'bondable': True, 'broadcast': True, 'bridgeable': True, 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$', } } @staticmethod 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), }, }} @staticmethod def get_bond_member_allowed_options() -> list: """ Return list of options which are allowed for changing, when interface is a bond member :return: List of interface options :rtype: list """ bond_allowed_sections = [ 'description', 'disable', 'disable_flow_control', 'disable_link_detect', 'duplex', 'eapol.ca_certificate', 'eapol.certificate', 'eapol.passphrase', 'mirror.egress', 'mirror.ingress', 'offload.gro', 'offload.gso', 'offload.lro', 'offload.rfs', 'offload.rps', 'offload.sg', 'offload.tso', 'redirect', 'ring_buffer.rx', 'ring_buffer.tx', 'speed', 'hw_id' ] return bond_allowed_sections def __init__(self, ifname, **kargs): super().__init__(ifname, **kargs) self.ethtool = Ethtool(ifname) def remove(self): """ Remove interface from config. Removing the interface deconfigures all assigned IP addresses. Example: >>> from vyos.ifconfig import WWANIf >>> i = EthernetIf('eth0') >>> i.remove() """ if self.exists(self.ifname): # interface is placed in A/D state when removed from config! It # will remain visible for the operating system. 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}.')]: Interface(vlan).remove() super().remove() def set_flow_control(self, enable): """ Changes the pause parameters of the specified Ethernet device. @param enable: true -> enable pause frames, false -> disable pause frames Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_flow_control(True) """ ifname = self.config['ifname'] if enable not in ['on', 'off']: 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!') return False current = self.ethtool.get_flow_control() if current != enable: # Assemble command executed on system. Unfortunately there is no way # to change this setting via sysfs cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' output, code = self._popen(cmd) if code: Warning(f'could not change "{ifname}" flow control setting!') return output return None def set_speed_duplex(self, speed, duplex): """ Set link speed in Mbit/s and duplex. @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto @duplex can be half, full, auto Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_speed_duplex('auto', 'auto') """ 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 duplex not in ['auto', 'full', 'half']: 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!') return if not self.ethtool.check_auto_negotiation_supported(): Warning(f'changing auto-negotiation setting on "{ifname}" is unsupported!') return # Get current speed and duplex settings: ifname = self.config['ifname'] if self.ethtool.get_auto_negotiation(): if speed == 'auto' and duplex == 'auto': # bail out early as nothing is to change return else: # XXX: read in current speed and duplex settings # There are some "nice" NICs like AX88179 which do not support # reading the speed thus we simply fallback to the supplied speed # to not cause any change here and raise an exception. cur_speed = read_file(f'/sys/class/net/{ifname}/speed', speed) cur_duplex = read_file(f'/sys/class/net/{ifname}/duplex', duplex) if (cur_speed == speed) and (cur_duplex == duplex): # bail out early as nothing is to change return cmd = f'ethtool --change {ifname}' try: if speed == 'auto' or duplex == 'auto': cmd += ' autoneg on' else: cmd += f' speed {speed} duplex {duplex} autoneg off' return self._cmd(cmd) except PermissionError: # Some NICs do not tell that they don't suppport settings 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!') def set_gro(self, state): """ Enable Generic Receive Offload. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_gro(True) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_generic_receive_offload() if enabled != 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!') return False def set_gso(self, state): """ Enable Generic Segmentation offload. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_gso(True) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_generic_segmentation_offload() if enabled != 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!') return False def set_hw_tc_offload(self, state): """ Enable hardware TC flow offload. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_hw_tc_offload(True) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_hw_tc_offload() if enabled != state: if not fixed: return self.set_interface('hw-tc-offload', 'on' if state else 'off') else: print('Adapter does not support changing hw-tc-offload settings!') return False def set_lro(self, state): """ Enable Large Receive offload. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_lro(True) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_large_receive_offload() if enabled != 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!') return False def set_rps(self, state): if not isinstance(state, bool): raise ValueError('Value out of range') rps_cpus = 0 queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*')) if state: # Enable RPS on all available CPUs except CPU0 which we will not # utilize so the system has one spare core when it's under high # preasure to server other means. Linux sysfs excepts a bitmask # representation of the CPUs which should participate on RPS, we # can enable more CPUs that are physically present on the system, # Linux will clip that internally! 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. # This is done by masking out the lowst bit so CPU0 is spared from # receive packet steering. rps_cpus &= ~1 for i in range(0, queues): self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', f'{rps_cpus:x}') # send bitmask representation as hex string without leading '0x' return True def set_rfs(self, state): rfs_flow = 0 queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*')) if state: global_rfs_flow = 32768 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) return True def set_sg(self, state): """ Enable Scatter-Gather support. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_sg(True) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_scatter_gather() if enabled != state: if not fixed: return self.set_interface('sg', 'on' if state else 'off') else: print('Adapter does not support changing scatter-gather settings!') return False def set_tso(self, state): """ Enable TCP segmentation offloading. State can be either True or False. Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_tso(False) """ if not isinstance(state, bool): raise ValueError('Value out of range') enabled, fixed = self.ethtool.get_tcp_segmentation_offload() if enabled != 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!') return False def set_ring_buffer(self, rx_tx, size): """ Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_ring_buffer('rx', '4096') """ current_size = self.ethtool.get_ring_buffer(rx_tx) if current_size == size: # bail out early if nothing is about to change return None ifname = self.config['ifname'] cmd = f'ethtool --set-ring {ifname} {rx_tx} {size}' 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}" ring-buffer for {ifname}') return output def set_eee(self, enable): """ Enable/Disable Energy Efficient Ethernet (EEE) settings Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_eee(enable=False) """ if not isinstance(enable, bool): raise ValueError('Value out of range') if not self.ethtool.check_eee(): self._debug_msg(f'NIC driver does not support changing EEE settings!') return False current = self.ethtool.get_eee() if current != enable: # Assemble command executed on system. Unfortunately there is no way # to change this setting via sysfs cmd = f'ethtool --set-eee {self.ifname} eee ' cmd += 'on' if enable else 'off' output, code = self._popen(cmd) if code: Warning(f'could not change "{self.ifname}" EEE setting!') return output return None 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 interface setup code and provide a single point of entry when workin on any interface. """ # disable ethernet flow control (pause frames) value = 'off' if 'disable_flow_control' in config else 'on' self.set_flow_control(value) # Always disable Energy Efficient Ethernet self.set_eee(False) # GRO (generic receive offload) self.set_gro(dict_search('offload.gro', config) != None) # GSO (generic segmentation offload) self.set_gso(dict_search('offload.gso', config) != None) # GSO (generic segmentation offload) self.set_hw_tc_offload(dict_search('offload.hw_tc_offload', config) != None) # LRO (large receive offload) self.set_lro(dict_search('offload.lro', config) != None) # RPS - Receive Packet Steering self.set_rps(dict_search('offload.rps', config) != None) # RFS - Receive Flow Steering self.set_rfs(dict_search('offload.rfs', config) != None) # scatter-gather option self.set_sg(dict_search('offload.sg', config) != None) # TSO (TCP segmentation offloading) self.set_tso(dict_search('offload.tso', config) != None) # Set physical interface speed and duplex if 'speed_duplex_changed' in config: if {'speed', 'duplex'} <= set(config): speed = config.get('speed') duplex = config.get('duplex') self.set_speed_duplex(speed, duplex) # Set interface ring buffer if 'ring_buffer' in config: for rx_tx, size in config['ring_buffer'].items(): self.set_ring_buffer(rx_tx, size) # call base class last super().update(config)