diff options
-rw-r--r-- | data/templates/vpp/override.conf.j2 | 14 | ||||
-rw-r--r-- | data/templates/vpp/startup.conf.j2 | 116 | ||||
-rw-r--r-- | debian/control | 5 | ||||
-rw-r--r-- | interface-definitions/vpp.xml.in | 342 | ||||
-rw-r--r-- | python/vyos/ethtool.py | 3 | ||||
-rw-r--r-- | python/vyos/vpp.py | 315 | ||||
-rwxr-xr-x | src/conf_mode/vpp.py | 183 |
7 files changed, 977 insertions, 1 deletions
diff --git a/data/templates/vpp/override.conf.j2 b/data/templates/vpp/override.conf.j2 new file mode 100644 index 000000000..a2c2b04ed --- /dev/null +++ b/data/templates/vpp/override.conf.j2 @@ -0,0 +1,14 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists= +ConditionPathExists=/run/vpp/vpp.conf + +[Service] +EnvironmentFile= +ExecStart= +ExecStart=/usr/bin/vpp -c /run/vpp/vpp.conf +WorkingDirectory= +WorkingDirectory=/run/vpp +Restart=always +RestartSec=10 diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 new file mode 100644 index 000000000..f33539fba --- /dev/null +++ b/data/templates/vpp/startup.conf.j2 @@ -0,0 +1,116 @@ +# Generated by /usr/libexec/vyos/conf_mode/vpp.py + +unix { + nodaemon + log /var/log/vpp.log + full-coredump + cli-listen /run/vpp/cli.sock + gid vpp + # exec /etc/vpp/bootstrap.vpp +{% if unix is vyos_defined %} +{% if unix.poll_sleep_usec is vyos_defined %} + poll-sleep-usec {{ unix.poll_sleep_usec }} +{% endif %} +{% endif %} +} + +{% if cpu is vyos_defined %} +cpu { +{% if cpu.main_core is vyos_defined %} + main-core {{ cpu.main_core }} +{% endif %} +{% if cpu.corelist_workers is vyos_defined %} + corelist-workers {{ cpu.corelist_workers | join(',') }} +{% endif %} +{% if cpu.skip_cores is vyos_defined %} + skip-cores {{ cpu.skip_cores }} +{% endif %} +{% if cpu.workers is vyos_defined %} + workers {{ cpu.workers }} +{% endif %} +} +{% endif %} + +{# ip heap-size does not work now (23.06-rc2~1-g3a4e62ad4) #} +{# vlib_call_all_config_functions: unknown input `ip heap-size 32M ' #} +{% if ip is vyos_defined %} +#ip { +#{% if ip.heap_size is vyos_defined %} +# heap-size {{ ip.heap_size }}M +#{% endif %} +#} +{% endif %} + +{% if ip6 is vyos_defined %} +ip6 { +{% if ip6.hash_buckets is vyos_defined %} + hash-buckets {{ ip6.hash_buckets }} +{% endif %} +{% if ip6.heap_size is vyos_defined %} + heap-size {{ ip6.heap_size }}M +{% endif %} +} +{% endif %} + +{% if l2learn is vyos_defined %} +l2learn { +{% if l2learn.limit is vyos_defined %} + limit {{ l2learn.limit }} +{% endif %} +} +{% endif %} + +{% if logging is vyos_defined %} +logging { +{% if logging.default_log_level is vyos_defined %} + default-log-level {{ logging.default_log_level }} +{% endif %} +} +{% endif %} + +{% if physmem is vyos_defined %} +physmem { +{% if physmem.max_size is vyos_defined %} + max-size {{ physmem.max_size.upper() }} +{% endif %} +} +{% endif %} + +plugins { + path /usr/lib/x86_64-linux-gnu/vpp_plugins/ + plugin default { disable } + plugin dpdk_plugin.so { enable } + plugin linux_cp_plugin.so { enable } + plugin linux_nl_plugin.so { enable } +} + +linux-cp { + lcp-sync + lcp-auto-subint +} + +dpdk { + # Whitelist the fake PCI address 0000:00:00.0 + # This prevents all devices from being added to VPP-DPDK by default + dev 0000:00:00.0 +{% for iface, iface_config in interface.items() %} +{% if iface_config.pci is vyos_defined %} + dev {{ iface_config.pci }} { + name {{ iface }} +{% if iface_config.num_rx_desc is vyos_defined %} + num-rx-desc {{ iface_config.num_rx_desc }} +{% endif %} +{% if iface_config.num_tx_desc is vyos_defined %} + num-tx-desc {{ iface_config.num_tx_desc }} +{% endif %} +{% if iface_config.num_rx_queues is vyos_defined %} + num-rx-queues {{ iface_config.num_rx_queues }} +{% endif %} +{% if iface_config.num_tx_queues is vyos_defined %} + num-tx-queues {{ iface_config.num_tx_queues }} +{% endif %} + } +{% endif %} +{% endfor %} + uio-bind-force +} diff --git a/debian/control b/debian/control index 9829ae9a3..40920cadc 100644 --- a/debian/control +++ b/debian/control @@ -90,6 +90,7 @@ Depends: libqmi-utils, libstrongswan-extra-plugins (>=5.9), libstrongswan-standard-plugins (>=5.9), + libvppinfra, libvyosconfig0, lldpd, lm-sensors, @@ -142,6 +143,7 @@ Depends: python3-tabulate, python3-vici (>= 5.7.2), python3-voluptuous, + python3-vpp-api, python3-xmltodict, python3-zmq, qrencode, @@ -176,6 +178,9 @@ Depends: uidmap, usb-modeswitch, usbutils, + vpp, + vpp-plugin-core, + vpp-plugin-dpdk, vyatta-bash, vyatta-cfg, vyos-http-api-tools, diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in new file mode 100644 index 000000000..51ab776c3 --- /dev/null +++ b/interface-definitions/vpp.xml.in @@ -0,0 +1,342 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpp" owner="${vyos_conf_scripts_dir}/vpp.py"> + <properties> + <help>Accelerated data-plane</help> + <priority>1280</priority> + </properties> + <children> + <node name="cpu"> + <properties> + <help>CPU settings</help> + </properties> + <children> + <leafNode name="corelist-workers"> + <properties> + <help>List of cores worker threads</help> + <valueHelp> + <format><id></format> + <description>CPU core id</description> + </valueHelp> + <valueHelp> + <format><idN>-<idM></format> + <description>CPU core id range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-512"/> + </constraint> + <constraintErrorMessage>not a valid CPU core value or range</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="main-core"> + <properties> + <help>Main core</help> + <valueHelp> + <format>u32:0-512</format> + <description>Assign main thread to specific core</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-512"/> + </constraint> + </properties> + </leafNode> + <leafNode name="skip-cores"> + <properties> + <help>Skip cores</help> + <valueHelp> + <format>u32:0-512</format> + <description>Skip cores</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-512"/> + </constraint> + </properties> + </leafNode> + <leafNode name="workers"> + <properties> + <help>Create worker threads</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Worker threads</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-512"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="interface"> + <properties> + <help>Interface</help> + <valueHelp> + <format>ethN</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <regex>((eth|lan)[0-9]+|(eno|ens|enp|enx).+)</regex> + </constraint> + <constraintErrorMessage>Invalid interface name</constraintErrorMessage> + </properties> + <children> + <leafNode name="num-rx-desc"> + <properties> + <help>Number of receive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of receive ring descriptors</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-tx-desc"> + <properties> + <help>Number of tranceive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of tranceive ring descriptors</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-rx-queues"> + <properties> + <help>Number of receive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of receive queues</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-tx-queues"> + <properties> + <help>Number of tranceive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of tranceive queues</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name='pci'> + <properties> + <help>PCI address allocation</help> + <valueHelp> + <format>auto</format> + <description>Auto detect PCI address</description> + </valueHelp> + <valueHelp> + <format><xxxx:xx:xx.x></format> + <description>Set Peripheral Component Interconnect (PCI) address</description> + </valueHelp> + <constraint> + <regex>(auto|[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])</regex> + </constraint> + </properties> + <defaultValue>auto</defaultValue> + </leafNode> + <leafNode name="rx-mode"> + <properties> + <help>Receive packet processing mode</help> + <completionHelp> + <list>polling interrupt adaptive</list> + </completionHelp> + <valueHelp> + <format>polling</format> + <description>Constantly check for new data</description> + </valueHelp> + <valueHelp> + <format>interrupt</format> + <description>Interrupt mode</description> + </valueHelp> + <valueHelp> + <format>adaptive</format> + <description>Adaptive mode</description> + </valueHelp> + <constraint> + <regex>(polling|interrupt|adaptive)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="ip"> + <properties> + <help>IP settings</help> + </properties> + <children> + <leafNode name="heap-size"> + <properties> + <help>IPv4 heap size</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Amount of memory (in Mbytes) dedicated to the destination IP lookup table</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>32</defaultValue> + </leafNode> + </children> + </node> + <node name="ip6"> + <properties> + <help>IPv6 settings</help> + </properties> + <children> + <leafNode name="heap-size"> + <properties> + <help>IPv6 heap size</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Amount of memory (in Mbytes) dedicated to the destination IP lookup table</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>32</defaultValue> + </leafNode> + <leafNode name="hash-buckets"> + <properties> + <help>IPv6 forwarding table hash buckets</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>IPv6 forwarding table hash buckets</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>65536</defaultValue> + </leafNode> + </children> + </node> + <node name="l2learn"> + <properties> + <help>Level 2 MAC address learning settings</help> + </properties> + <children> + <leafNode name="limit"> + <properties> + <help>Number of MAC addresses in the L2 FIB</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>Number of concurent entries</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>4194304</defaultValue> + </leafNode> + </children> + </node> + <node name="logging"> + <properties> + <help>Loggint settings</help> + </properties> + <children> + <leafNode name="default-log-level"> + <properties> + <help>default-log-level</help> + <completionHelp> + <list>alert crit debug disabled emerg err info notice warn</list> + </completionHelp> + <valueHelp> + <format>alert</format> + <description>Alert</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug</description> + </valueHelp> + <valueHelp> + <format>disabled</format> + <description>Disabled</description> + </valueHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Notice</description> + </valueHelp> + <valueHelp> + <format>warn</format> + <description>Warning</description> + </valueHelp> + <constraint> + <regex>(alert|crit|debug|disabled|emerg|err|info|notice|warn)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="physmem"> + <properties> + <help>Memory settings</help> + </properties> + <children> + <leafNode name="max-size"> + <properties> + <help>Set memory size for protectable memory allocator (pmalloc) memory space</help> + <valueHelp> + <format><number>m</format> + <description>Megabyte</description> + </valueHelp> + <valueHelp> + <format><number>g</format> + <description>Gigabyte</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <node name="unix"> + <properties> + <help>Unix settings</help> + </properties> + <children> + <leafNode name="poll-sleep-usec"> + <properties> + <help>Add a fixed-sleep between main loop poll</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Number of receive queues</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 68234089c..9b7da89fa 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -21,7 +21,8 @@ from vyos.util import popen # These drivers do not support using ethtool to change the speed, duplex, or # flow control settings _drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront', - 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf'] + 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf', + 'tun'] class Ethtool: """ diff --git a/python/vyos/vpp.py b/python/vyos/vpp.py new file mode 100644 index 000000000..76e5d29c3 --- /dev/null +++ b/python/vyos/vpp.py @@ -0,0 +1,315 @@ +# Copyright 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/>. + +from functools import wraps +from pathlib import Path +from re import search as re_search, fullmatch as re_fullmatch, MULTILINE as re_M +from subprocess import run +from time import sleep + +from vpp_papi import VPPApiClient +from vpp_papi import VPPIOError, VPPValueError + + +class VPPControl: + """Control VPP network stack + """ + + class _Decorators: + """Decorators for VPPControl + """ + + @classmethod + def api_call(cls, decorated_func): + """Check if API is connected before API call + + Args: + decorated_func: function to decorate + + Raises: + VPPIOError: Connection to API is not established + """ + + @wraps(decorated_func) + def api_safe_wrapper(cls, *args, **kwargs): + if not cls.vpp_api_client.transport.connected: + raise VPPIOError(2, 'VPP API is not connected') + return decorated_func(cls, *args, **kwargs) + + return api_safe_wrapper + + @classmethod + def check_retval(cls, decorated_func): + """Check retval from API response + + Args: + decorated_func: function to decorate + + Raises: + VPPValueError: raised when retval is not 0 + """ + + @wraps(decorated_func) + def check_retval_wrapper(cls, *args, **kwargs): + return_value = decorated_func(cls, *args, **kwargs) + if not return_value.retval == 0: + raise VPPValueError( + f'VPP API call failed: {return_value.retval}') + return return_value + + return check_retval_wrapper + + def __init__(self, attempts: int = 5, interval: int = 1000) -> None: + """Create VPP API connection + + Args: + attempts (int, optional): attempts to connect. Defaults to 5. + interval (int, optional): interval between attempts in ms. Defaults to 1000. + + Raises: + VPPIOError: Connection to API cannot be established + """ + self.vpp_api_client = VPPApiClient() + # connect with interval + while attempts: + try: + attempts -= 1 + self.vpp_api_client.connect('vpp-vyos') + break + except (ConnectionRefusedError, FileNotFoundError) as err: + print(f'VPP API connection timeout: {err}') + sleep(interval / 1000) + # raise exception if connection was not successful in the end + if not self.vpp_api_client.transport.connected: + raise VPPIOError(2, 'Cannot connect to VPP API') + + def __del__(self) -> None: + """Disconnect from VPP API (destructor) + """ + self.disconnect() + + def disconnect(self) -> None: + """Disconnect from VPP API + """ + if self.vpp_api_client.transport.connected: + self.vpp_api_client.disconnect() + + @_Decorators.check_retval + @_Decorators.api_call + def cli_cmd(self, command: str): + """Send raw CLI command + + Args: + command (str): command to send + + Returns: + vpp_papi.vpp_serializer.cli_inband_reply: CLI reply class + """ + return self.vpp_api_client.api.cli_inband(cmd=command) + + @_Decorators.api_call + def get_mac(self, ifname: str) -> str: + """Find MAC address by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + str: MAC address + """ + for iface in self.vpp_api_client.api.sw_interface_dump(): + if iface.interface_name == ifname: + return iface.l2_address.mac_string + return '' + + @_Decorators.api_call + def get_sw_if_index(self, ifname: str) -> int | None: + """Find interface index by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + int | None: Interface index or None (if was not fount) + """ + for iface in self.vpp_api_client.api.sw_interface_dump(): + if iface.interface_name == ifname: + return iface.sw_if_index + return None + + @_Decorators.check_retval + @_Decorators.api_call + def lcp_pair_add(self, iface_name_vpp: str, iface_name_kernel: str) -> None: + """Create LCP interface pair between VPP and kernel + + Args: + iface_name_vpp (str): interface name in VPP + iface_name_kernel (str): interface name in kernel + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + if iface_index: + return self.vpp_api_client.api.lcp_itf_pair_add_del( + is_add=True, + sw_if_index=iface_index, + host_if_name=iface_name_kernel) + + @_Decorators.check_retval + @_Decorators.api_call + def lcp_pair_del(self, iface_name_vpp: str, iface_name_kernel: str) -> None: + """Delete LCP interface pair between VPP and kernel + + Args: + iface_name_vpp (str): interface name in VPP + iface_name_kernel (str): interface name in kernel + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + if iface_index: + return self.vpp_api_client.api.lcp_itf_pair_add_del( + is_add=False, + sw_if_index=iface_index, + host_if_name=iface_name_kernel) + + @_Decorators.check_retval + @_Decorators.api_call + def iface_rxmode(self, iface_name: str, rx_mode: str) -> None: + """Set interface rx-mode in VPP + + Args: + iface_name (str): interface name in VPP + rx_mode (str): mode (polling, interrupt, adaptive) + """ + modes_dict: dict[str, int] = { + 'polling': 1, + 'interrupt': 2, + 'adaptive': 3 + } + if rx_mode not in modes_dict: + raise VPPValueError(f'Mode {rx_mode} is not known') + iface_index = self.get_sw_if_index(iface_name) + return self.vpp_api_client.api.sw_interface_set_rx_mode( + sw_if_index=iface_index, mode=modes_dict[rx_mode]) + + @_Decorators.api_call + def get_pci_addr(self, ifname: str) -> str: + """Find PCI address of interface by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + str: PCI address + """ + hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply + + regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$' + re_obj = re_search(regex_filter, hw_info, re_M) + + # return empty string if no interface or no PCI info was found + if not hw_info or not re_obj: + return '' + + address = re_obj.groupdict().get('address', '') + + # we need to modify address to math kernel style + # for example: 0000:06:14.00 -> 0000:06:14.0 + address_chunks: list[str] = address.split('.') + address_normalized: str = f'{address_chunks[0]}.{int(address_chunks[1])}' + + return address_normalized + + +class HostControl: + """Control Linux host + """ + + @staticmethod + def pci_rescan(pci_addr: str = '') -> None: + """Rescan PCI device by removing it and rescan PCI bus + + If PCI address is not defined - just rescan PCI bus + + Args: + address (str, optional): PCI address of device. Defaults to ''. + """ + if pci_addr: + device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove') + if device_file.exists(): + device_file.write_text('1') + # wait 10 seconds max until device will be removed + attempts = 100 + while device_file.exists() and attempts: + attempts -= 1 + sleep(0.1) + if device_file.exists(): + raise TimeoutError( + f'Timeout was reached for removing PCI device {pci_addr}' + ) + else: + raise FileNotFoundError(f'PCI device {pci_addr} does not exist') + rescan_file = Path('/sys/bus/pci/rescan') + rescan_file.write_text('1') + if pci_addr: + # wait 10 seconds max until device will be installed + attempts = 100 + while not device_file.exists() and attempts: + attempts -= 1 + sleep(0.1) + if not device_file.exists(): + raise TimeoutError( + f'Timeout was reached for installing PCI device {pci_addr}') + + @staticmethod + def get_eth_name(pci_addr: str) -> str: + """Find Ethernet interface name by PCI address + + Args: + pci_addr (str): PCI address + + Raises: + FileNotFoundError: no Ethernet interface was found + + Returns: + str: Ethernet interface name + """ + # find all PCI devices with eth* names + net_devs: dict[str, str] = {} + net_devs_dir = Path('/sys/class/net') + regex_filter = r'^/sys/devices/pci[\w/:\.]+/(?P<pci_addr>\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?P<iface_name>eth\d+)$' + for dir in net_devs_dir.iterdir(): + real_dir: str = dir.resolve().as_posix() + re_obj = re_fullmatch(regex_filter, real_dir) + if re_obj: + iface_name: str = re_obj.group('iface_name') + iface_addr: str = re_obj.group('pci_addr') + net_devs.update({iface_addr: iface_name}) + # match to provided PCI address and return a name if found + if pci_addr in net_devs: + return net_devs[pci_addr] + # raise error if device was not found + raise FileNotFoundError( + f'PCI device {pci_addr} not found in ethernet interfaces') + + @staticmethod + def rename_iface(name_old: str, name_new: str) -> None: + """Rename interface + + Args: + name_old (str): old name + name_new (str): new name + """ + rename_cmd: list[str] = [ + 'ip', 'link', 'set', name_old, 'name', name_new + ] + run(rename_cmd) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py new file mode 100755 index 000000000..dd01da87e --- /dev/null +++ b/src/conf_mode/vpp.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from pathlib import Path +from re import search as re_search, MULTILINE as re_M + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.ifconfig import Section +from vyos.ifconfig import EthernetIf +from vyos.ifconfig import interface +from vyos.util import call +from vyos.util import rc_cmd +from vyos.template import render +from vyos.xml import defaults + +from vyos import ConfigError +from vyos import airbag +from vyos.vpp import VPPControl +from vyos.vpp import HostControl + +airbag.enable() + +service_name = 'vpp' +service_conf = Path(f'/run/vpp/{service_name}.conf') +systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' + + +def _get_pci_address_by_interface(iface) -> str: + from vyos.util import rc_cmd + rc, out = rc_cmd(f'ethtool -i {iface}') + # if ethtool command was successful + if rc == 0 and out: + regex_filter = r'^bus-info: (?P<address>\w+:\w+:\w+\.\w+)$' + re_obj = re_search(regex_filter, out, re_M) + # if bus-info with PCI address found + if re_obj: + address = re_obj.groupdict().get('address', '') + return address + # use VPP - maybe interface already attached to it + vpp_control = VPPControl(attempts=20, interval=500) + pci_addr = vpp_control.get_pci_addr(iface) + if pci_addr: + return pci_addr + # raise error if PCI address was not found + raise ConfigError(f'Cannot find PCI address for interface {iface}') + + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['vpp'] + base_ethernet = ['interfaces', 'ethernet'] + + # find interfaces removed from VPP + removed_ifaces = [] + tmp = node_changed(conf, base + ['interface']) + if tmp: + for removed_iface in tmp: + pci_address: str = _get_pci_address_by_interface(removed_iface) + removed_ifaces.append({ + 'iface_name': removed_iface, + 'iface_pci_addr': pci_address + }) + + if not conf.exists(base): + return {'removed_ifaces': removed_ifaces} + + config = conf.get_config_dict(base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + if 'interface' in default_values: + del default_values['interface'] + config = dict_merge(default_values, config) + + if 'interface' in config: + for iface, iface_config in config['interface'].items(): + default_values_iface = defaults(base + ['interface']) + config['interface'][iface] = dict_merge(default_values_iface, config['interface'][iface]) + + # Get PCI address auto + for iface, iface_config in config['interface'].items(): + if iface_config['pci'] == 'auto': + config['interface'][iface]['pci'] = _get_pci_address_by_interface(iface) + + config['other_interfaces'] = conf.get_config_dict(base_ethernet, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + if removed_ifaces: + config['removed_ifaces'] = removed_ifaces + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if not config or (len(config) == 1 and 'removed_ifaces' in config): + return None + + if 'interface' not in config: + raise ConfigError(f'"interface" is required but not set!') + + if 'cpu' in config: + if 'corelist_workers' in config['cpu'] and 'main_core' not in config['cpu']: + raise ConfigError(f'"cpu main-core" is required but not set!') + + +def generate(config): + if not config or (len(config) == 1 and 'removed_ifaces' in config): + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + render(service_conf, 'vpp/startup.conf.j2', config) + render(systemd_override, 'vpp/override.conf.j2', config) + + return None + + +def apply(config): + if not config or (len(config) == 1 and 'removed_ifaces' in config): + call(f'systemctl stop {service_name}.service') + else: + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + + # Initialize interfaces removed from VPP + for iface in config.get('removed_ifaces', []): + host_control = HostControl() + # rescan PCI to use a proper driver + host_control.pci_rescan(iface['iface_pci_addr']) + # rename to the proper name + iface_new_name: str = host_control.get_eth_name(iface['iface_pci_addr']) + host_control.rename_iface(iface_new_name, iface['iface_name']) + + if 'interface' in config: + # connect to VPP + # must be performed multiple attempts because API is not available + # immediately after the service restart + vpp_control = VPPControl(attempts=20, interval=500) + for iface, _ in config['interface'].items(): + # Create lcp + if iface not in Section.interfaces(): + vpp_control.lcp_pair_add(iface, iface) + + # update interface config + #e = EthernetIf(iface) + #e.update(config['other_interfaces'][iface]) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |