diff options
-rw-r--r-- | .github/workflows/cleanup-mirror-pr-branch.yml | 26 | ||||
-rw-r--r-- | .github/workflows/trigger-pr-mirror-repo-sync.yml | 32 | ||||
-rw-r--r-- | interface-definitions/interfaces_ethernet.xml.in | 6 | ||||
-rw-r--r-- | python/vyos/ifconfig/ethernet.py | 173 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_vyostest_shim.py | 4 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 43 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dns_dynamic.py | 2 |
7 files changed, 167 insertions, 119 deletions
diff --git a/.github/workflows/cleanup-mirror-pr-branch.yml b/.github/workflows/cleanup-mirror-pr-branch.yml index c5de9ab73..bbe6aa2f2 100644 --- a/.github/workflows/cleanup-mirror-pr-branch.yml +++ b/.github/workflows/cleanup-mirror-pr-branch.yml @@ -5,31 +5,11 @@ on: types: [closed] branches: - current - workflow_dispatch: - inputs: - branch: - description: 'Branch to delete' - required: true permissions: contents: write jobs: - delete_branch: - if: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.event.pull_request.head.ref, 'mirror/')) && github.repository_owner != 'vyos' }} - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Delete branch - run: | - branch=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.event.pull_request.head.ref }} - if [[ $branch != mirror/* ]]; then - echo "Branch name to clean must start with 'mirror/'" - exit 1 - fi - repo=${{ github.repository }} - git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} - git push origin --delete $branch + call-delete-branch: + uses: vyos/.github/.github/workflows/cleanup-mirror-pr-branch.yml@current + secrets: inherit diff --git a/.github/workflows/trigger-pr-mirror-repo-sync.yml b/.github/workflows/trigger-pr-mirror-repo-sync.yml index 9653c2dca..d5e8ce3b4 100644 --- a/.github/workflows/trigger-pr-mirror-repo-sync.yml +++ b/.github/workflows/trigger-pr-mirror-repo-sync.yml @@ -6,33 +6,7 @@ on: branches: - current -env: - GH_TOKEN: ${{ secrets.PAT }} - -concurrency: - group: trigger-pr-mirror-repo-sync-${{ github.event.pull_request.base.ref }} - cancel-in-progress: false jobs: - trigger-mirror-pr-repo-sync: - if: ${{ github.repository_owner == 'vyos' }} - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: write - - steps: - - name: Bullfrog Secure Runner - uses: bullfrogsec/bullfrog@v0 - with: - egress-policy: audit - - - name: Trigger repo sync - shell: bash - run: | - echo "Triggering sync workflow for ${{ secrets.REMOTE_OWNER }}/${{ secrets.REMOTE_REPO }}" - echo "Triggering sync workflow with PAT ${{ secrets.PAT }}" - curl -X POST \ - -H "Accept: application/vnd.github.everest-preview+json" \ - -H "Authorization: Bearer ${{ secrets.PAT }}" \ - https://api.github.com/repos/${{ secrets.REMOTE_OWNER }}/${{ secrets.REMOTE_REPO }}/actions/workflows/mirror-pr-and-sync.yml/dispatches \ - -d '{"ref":"git-actions", "inputs": {"pr_number": "${{ github.event.pull_request.number }}", "sync_branch": "${{ github.event.pull_request.base.ref }}"}}' + call-trigger-mirror-pr-repo-sync: + uses: vyos/.github/.github/workflows/trigger-pr-mirror-repo-sync.yml@current + secrets: inherit 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/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index d95071d1a..9cf6a653a 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -27,6 +27,7 @@ from vyos import ConfigError from vyos.defaults import commit_lock from vyos.utils.process import cmd from vyos.utils.process import run +from vyos.utils.process import process_named_running save_config = '/tmp/vyos-smoketest-save' @@ -88,6 +89,9 @@ class VyOSUnitTestSHIM: # during a commit there is a process opening commit_lock, and run() returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) + # wait for FRR reload to be complete + while process_named_running('frr-reload.py'): + sleep(0.250) # reset getFRRconfig() guard timer self.commit_guard = time() 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) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 3ce459e44..28380f6d4 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -44,7 +44,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process - self.assertTrue(process_named_running(DDCLIENT_PNAME)) + self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=5)) # Delete DDNS configuration self.cli_delete(base_path) |