summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/vpp/override.conf.j214
-rw-r--r--data/templates/vpp/startup.conf.j2116
-rw-r--r--debian/control5
-rw-r--r--interface-definitions/vpp.xml.in342
-rw-r--r--python/vyos/ethtool.py3
-rw-r--r--python/vyos/vpp.py315
-rwxr-xr-xsrc/conf_mode/vpp.py183
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>&lt;id&gt;</format>
+ <description>CPU core id</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;idN&gt;-&lt;idM&gt;</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>&lt;xxxx:xx:xx.x&gt;</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>&lt;number&gt;m</format>
+ <description>Megabyte</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;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)