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) | 
