summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2025-01-07 21:54:47 +0000
committerViacheslav Hletenko <v.gletenko@vyos.io>2025-01-07 22:30:38 +0000
commitbb4427bfb4a8c03784dd6f097cbe521f561edaf4 (patch)
tree5773be78c736903e21d31eed4af31cd2e58f7096
parentdef342ffc4244e345f02685c12dc157372d5dd68 (diff)
downloadvyos-1x-bb4427bfb4a8c03784dd6f097cbe521f561edaf4.tar.gz
vyos-1x-bb4427bfb4a8c03784dd6f097cbe521f561edaf4.zip
Add vyos-vpp CLI and python3 modules
-rw-r--r--data/config-mode-dependencies/vyos-vpp.json43
-rw-r--r--data/templates/vpp/override.conf.j214
-rw-r--r--data/templates/vpp/startup.conf.j2181
-rw-r--r--python/vyos/vpp/__init__.py20
-rw-r--r--python/vyos/vpp/config_deps.py76
-rw-r--r--python/vyos/vpp/config_filter.py58
-rw-r--r--python/vyos/vpp/config_verify.py160
-rw-r--r--python/vyos/vpp/configdb.py206
-rw-r--r--python/vyos/vpp/control_host.py363
-rw-r--r--python/vyos/vpp/control_vpp.py444
-rw-r--r--python/vyos/vpp/interface/__init__.py40
-rw-r--r--python/vyos/vpp/interface/bond.py113
-rw-r--r--python/vyos/vpp/interface/bridge.py128
-rw-r--r--python/vyos/vpp/interface/ethernet.py54
-rw-r--r--python/vyos/vpp/interface/geneve.py90
-rw-r--r--python/vyos/vpp/interface/gre.py85
-rw-r--r--python/vyos/vpp/interface/ipip.py83
-rw-r--r--python/vyos/vpp/interface/loopback.py68
-rw-r--r--python/vyos/vpp/interface/vxlan.py94
-rw-r--r--python/vyos/vpp/interface/wireguard.py56
-rw-r--r--python/vyos/vpp/interface/xconnect.py94
-rw-r--r--python/vyos/vpp/nat/__init__.py0
-rw-r--r--python/vyos/vpp/utils.py281
-rwxr-xr-xsmoketest/scripts/cli/test_vpp.py914
-rw-r--r--src/completion/list_vpp_interfaces.py39
-rwxr-xr-xsrc/conf_mode/vpp.py553
-rw-r--r--src/conf_mode/vpp_interfaces_bonding.py215
-rw-r--r--src/conf_mode/vpp_interfaces_bridge.py158
-rw-r--r--src/conf_mode/vpp_interfaces_ethernet.py135
-rw-r--r--src/conf_mode/vpp_interfaces_geneve.py182
-rw-r--r--src/conf_mode/vpp_interfaces_gre.py182
-rw-r--r--src/conf_mode/vpp_interfaces_ipip.py181
-rw-r--r--src/conf_mode/vpp_interfaces_loopback.py153
-rw-r--r--src/conf_mode/vpp_interfaces_vxlan.py188
-rw-r--r--src/conf_mode/vpp_interfaces_xconnect.py142
-rw-r--r--src/conf_mode/vpp_kernel-interfaces.py192
-rwxr-xr-xsrc/op_mode/show_vpp_interfaces.py257
37 files changed, 6242 insertions, 0 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json
new file mode 100644
index 000000000..04dc4a771
--- /dev/null
+++ b/data/config-mode-dependencies/vyos-vpp.json
@@ -0,0 +1,43 @@
+{
+ "vpp": {
+ "ethernet": ["interfaces_ethernet"],
+ "vpp_interfaces_bonding": ["vpp_interfaces_bonding"],
+ "vpp_interfaces_bridge": ["vpp_interfaces_bridge"],
+ "vpp_interfaces_ethernet": ["vpp_interfaces_ethernet"],
+ "vpp_interfaces_geneve": ["vpp_interfaces_geneve"],
+ "vpp_interfaces_gre": ["vpp_interfaces_gre"],
+ "vpp_interfaces_ipip": ["vpp_interfaces_ipip"],
+ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"],
+ "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"],
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_bonding": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_ethernet": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_geneve": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_gre": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_ipip": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_loopback": {
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_vxlan": {
+ "vpp_interfaces_bridge": ["vpp_interfaces_bridge"],
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ }
+}
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..c1dac7757
--- /dev/null
+++ b/data/templates/vpp/startup.conf.j2
@@ -0,0 +1,181 @@
+# Generated by ${vyos_conf_scripts_dir}/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 %}
+
+{% if buffers is vyos_defined %}
+buffers {
+{% if buffers.buffers_per_numa is vyos_defined %}
+ buffers-per-numa {{ buffers.buffers_per_numa }}
+{% endif %}
+{% if buffers.data_size is vyos_defined %}
+ default data-size {{ buffers.data_size }}
+{% endif %}
+{% if buffers.page_size is vyos_defined %}
+ page-size {{ buffers.page_size }}
+{% 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 }}
+{% 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 }}
+ default-syslog-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 }}
+{% endif %}
+}
+{% endif %}
+
+plugins {
+ path /usr/lib/x86_64-linux-gnu/vpp_plugins/
+ plugin default { disable }
+ plugin af_xdp_plugin.so { enable }
+ plugin avf_plugin.so { enable }
+ plugin dpdk_plugin.so { enable }
+ plugin vmxnet3_plugin.so { enable }
+ plugin lacp_plugin.so { enable }
+ plugin linux_cp_plugin.so { enable }
+ plugin linux_nl_plugin.so { enable }
+ plugin pppoe_plugin.so { enable }
+ # NAT uncomment if needed
+ # plugin cnat_plugin.so { enable }
+ # plugin nat_plugin.so { enable }
+ # plugin nat44_plugin.so { enable }
+ # plugin nat44_ei_plugin.so { enable }
+ # plugin nat64_plugin.so { enable }
+ # plugin nat66_plugin.so { enable }
+ # plugin pnat_plugin.so { enable }
+ # Tunnels
+ plugin geneve_plugin.so { enable }
+ plugin gre_plugin.so { enable }
+ plugin vxlan_plugin.so { enable }
+ plugin wireguard_plugin.so { enable }
+}
+
+linux-cp {
+ lcp-sync
+ lcp-auto-subint
+{% if lcp.route_no_paths is vyos_defined %}
+ route-no-paths
+{% endif %}
+}
+
+{% if lcp.netlink is vyos_defined %}
+linux-nl {
+{% if lcp.netlink.rx_buffer_size is vyos_defined %}
+ nl-rx-buffer-size {{ lcp.netlink.rx_buffer_size }}
+{% endif %}
+{% if lcp.netlink.batch_size is vyos_defined %}
+ nl-batch-size {{ lcp.netlink.batch_size }}
+{% endif %}
+{% if lcp.netlink.batch_delay_ms is vyos_defined %}
+ nl-batch-delay-ms {{ lcp.netlink.batch_delay_ms }}
+{% endif %}
+}
+{% endif %}
+
+{% if statseg is vyos_defined %}
+statseg {
+{% if statseg.size is vyos_defined %}
+ size {{ statseg.size }}
+{% endif %}
+{% if statseg.page_size is vyos_defined %}
+ page-size {{ statseg.page_size }}
+{% endif %}
+}
+{% endif %}
+
+{% if memory is vyos_defined %}
+memory {
+{% if memory.main_heap_size is vyos_defined %}
+ main-heap-size {{ memory.main_heap_size }}
+{% endif %}
+{% if memory.main_heap_page_size is vyos_defined %}
+ main-heap-page-size {{ memory.main_heap_page_size }}
+{% endif %}
+{% if memory.default_hugepage_size is vyos_defined %}
+ default-hugepage-size {{ memory.default_hugepage_size }}
+{% endif %}
+}
+{% endif %}
+
+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.driver == 'dpdk' %}
+ dev {{ iface_config.dpdk_options.dev_id }} {
+ name {{ iface }}
+{% if iface_config.dpdk_options.num_rx_desc is vyos_defined %}
+ num-rx-desc {{ iface_config.dpdk_options.num_rx_desc }}
+{% endif %}
+{% if iface_config.dpdk_options.num_tx_desc is vyos_defined %}
+ num-tx-desc {{ iface_config.dpdk_options.num_tx_desc }}
+{% endif %}
+{% if iface_config.dpdk_options.num_rx_queues is vyos_defined %}
+ num-rx-queues {{ iface_config.dpdk_options.num_rx_queues }}
+{% endif %}
+{% if iface_config.dpdk_options.num_tx_queues is vyos_defined %}
+ num-tx-queues {{ iface_config.dpdk_options.num_tx_queues }}
+{% endif %}
+ }
+{% endif %}
+{% endfor %}
+ uio-bind-force
+}
diff --git a/python/vyos/vpp/__init__.py b/python/vyos/vpp/__init__.py
new file mode 100644
index 000000000..a320ac7d1
--- /dev/null
+++ b/python/vyos/vpp/__init__.py
@@ -0,0 +1,20 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from .control_vpp import VPPControl
+
+__all__ = ['VPPControl']
diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py
new file mode 100644
index 000000000..d826dedbb
--- /dev/null
+++ b/python/vyos/vpp/config_deps.py
@@ -0,0 +1,76 @@
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+def deps_xconnect_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all xconnect interface members:
+
+ keys: members
+
+ values: xconnect interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ xconn_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'xconnect'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for xconn_name, xconn_config in config.items():
+ for member_name in xconn_config.get('member', {}).get('interface', []):
+ xconn_ifaces_list = xconn_members_dict.get(xconn_name, [])
+ xconn_ifaces_list.append(xconn_name)
+ xconn_members_dict.update({member_name: xconn_ifaces_list})
+
+ return xconn_members_dict
+
+
+def deps_bridge_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all bridge interface members:
+
+ keys: members
+
+ values: bridge interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ bridge_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'bridge'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for bridge_name, bridge_config in config.items():
+ for member_name in bridge_config.get('member', {}).get('interface', []):
+ bridge_ifaces_list = bridge_members_dict.get(bridge_name, [])
+ bridge_ifaces_list.append(bridge_name)
+ bridge_members_dict.update({member_name: bridge_ifaces_list})
+
+ return bridge_members_dict
diff --git a/python/vyos/vpp/config_filter.py b/python/vyos/vpp/config_filter.py
new file mode 100644
index 000000000..4ab9ffda2
--- /dev/null
+++ b/python/vyos/vpp/config_filter.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.config import Config
+
+
+def iface_filter_eth(config: Config, iface: str) -> None:
+ """Filter out unsupported config nodes from Ethernet interface config
+
+ Args:
+ config (Config): config object
+ iface (str): Ethernet interface name to filter
+ """
+ allowed_nodes: list[str] = [
+ 'address',
+ 'description',
+ 'dhcp-options',
+ 'dhcpv6-options',
+ 'disable',
+ 'eapol',
+ 'hw-id',
+ 'ip',
+ 'ipv6',
+ 'mtu',
+ 'redirect',
+ 'vif',
+ 'vif-s',
+ 'vrf',
+ ]
+
+ # get list of config nides in a session configuration
+ iface_nodes = config._session_config.list_nodes(['interfaces', 'ethernet', iface])
+
+ # clean cached session config
+ if False in config._dict_cache:
+ del config._dict_cache[False]
+
+ # remove unsupported config nodes
+ for cfg_node in iface_nodes:
+ if cfg_node not in allowed_nodes:
+ config._session_config.delete(['interfaces', 'ethernet', iface, cfg_node])
+ print(
+ f'WARNING: {cfg_node} option in {iface} settings is not supported by VPP interfaces. It will be ignored.'
+ )
diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py
new file mode 100644
index 000000000..6b35858d2
--- /dev/null
+++ b/python/vyos/vpp/config_verify.py
@@ -0,0 +1,160 @@
+# Used for verifying configuration vpp interfaces
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos import ConfigError
+
+from vyos.vpp.control_host import get_eth_driver
+
+
+def verify_vpp_remove_kernel_interface(config: dict):
+ """Common verify for removed kernel-interfaces.
+ Verify that removed kernel interface are not used in 'vpp kernel-interfaces'.
+
+ Example:
+ delete vpp interfaces gre|vxlan <tag>X kernel-interface vpp-tunX
+ set vpp kernel-interface vpp-tunX
+ """
+ if (
+ 'remove' in config
+ and 'kernel_interface_removed' in config
+ and 'vpp_kernel_interfaces' in config
+ ):
+ removed_interfaces = config['kernel_interface_removed']
+ used_interfaces = config['vpp_kernel_interfaces']
+
+ for interface in removed_interfaces:
+ if interface in used_interfaces:
+ raise ConfigError(
+ f'"{interface}" is still in use within "vpp kernel-interfaces". '
+ 'Please remove it before proceeding.'
+ )
+
+
+def verify_vpp_change_kernel_interface(config: dict):
+ """Common verify for changed kernel-interface
+
+ Example:
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunX'
+ commit
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunY'
+ commit
+
+ check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ """
+ kernel_interface_removed = config.get('kernel_interface_removed', [])
+ vpp_kernel_interfaces = config.get('vpp_kernel_interfaces', {})
+
+ for interface in kernel_interface_removed:
+ if interface in vpp_kernel_interfaces:
+ raise ConfigError(
+ f'interface "{interface}" is still in use within "vpp kernel-interfaces". '
+ f'Please remove it "vpp kernel-interface {interface}" before proceeding.'
+ )
+
+
+def verify_vpp_exists_kernel_interface(config: dict):
+ """Verify is a kernel-interface already created by another VPP LCP pair
+
+ Example:
+ set vpp interfaces vxlan vxlan10 kernel-interface vpp-tun10'
+ commit
+ set vpp interfaces vxlan vxlan20 kernel-interface vpp-tun10'
+ commit
+ """
+ kernel_interface = config.get('kernel_interface', '')
+ vpp_interface = config.get('ifname', '')
+ candidate_kernel_interfaces = config.get('candidate_kernel_interfaces', [])
+
+ for candidate_kernel_iface in candidate_kernel_interfaces:
+ if (
+ vpp_interface != candidate_kernel_iface[0]
+ and kernel_interface == candidate_kernel_iface[1]
+ ):
+ raise ConfigError(
+ f'Kernel interface "{kernel_interface}" is already configured for {candidate_kernel_iface[0]}. '
+ 'Duplicates are not allowed.'
+ )
+
+
+def verify_vpp_remove_xconnect_interface(config: dict):
+ if not config.get('remove'):
+ return
+ for xconn_member, xconn_iface in config.get('xconn_members').items():
+ if xconn_member == config.get('ifname'):
+ raise ConfigError(
+ f'interface "{xconn_member}" is still in use within "vpp interfaces xconnect". '
+ f'Please remove it from "vpp interface xconnect {xconn_iface}" before proceeding.'
+ )
+
+
+def verify_dev_driver(iface_name: str, driver_type: str) -> bool:
+ # Lists of drivers compatible with DPDK and XDP
+ drivers_dpdk: list[str] = [
+ 'atlantic',
+ 'bnx2x',
+ 'e1000',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igc',
+ 'ixgbe',
+ 'liquidio',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ drivers_xdp: list[str] = [
+ 'atlantic',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igb',
+ 'igc',
+ 'ixgbe',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ driver: str = get_eth_driver(iface_name)
+
+ if driver_type == 'dpdk':
+ if driver in drivers_dpdk:
+ return True
+ elif driver_type == 'xdp':
+ if driver in drivers_xdp:
+ return True
+ else:
+ raise ConfigError(f'"Driver type {driver_type} is wrong')
+
+ return False
diff --git a/python/vyos/vpp/configdb.py b/python/vyos/vpp/configdb.py
new file mode 100644
index 000000000..1830937ad
--- /dev/null
+++ b/python/vyos/vpp/configdb.py
@@ -0,0 +1,206 @@
+#
+# Copyright (C) 2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from json import loads as json_loads, dumps as json_dumps
+from pathlib import Path
+from typing import Any
+
+STORAGE_LOCATION = '/run/vpp'
+
+
+class JSONStorage:
+ def __init__(self, name: str = '') -> None:
+ """Initiate a file storage
+
+ Args:
+ name (str, optional): Unique storage name. Defaults to '' (generate a name).
+
+ Raises:
+ err: In case a file for storage cannot be created
+ """
+ # If a name is not provided, this is a temporary one-time storage
+ # this use case is strange, but let's allow this
+ if not name:
+ self.__temporary = True
+ self.__cache: dict[Any, Any] = {}
+ self.__locked = False
+ return
+ self.__temporary = False
+
+ self.__storage = Path(f'{STORAGE_LOCATION}/{name}.json')
+ self.__lock_file = Path(f'{STORAGE_LOCATION}/{name}.lock')
+
+ # prepare a folder
+ storage_dir = Path(STORAGE_LOCATION)
+ if not storage_dir.exists():
+ storage_dir.mkdir(parents=True)
+
+ # initialize lock status
+ self.__locked = False
+ if self.__storage_locked():
+ raise FileExistsError(f'Cannot open locked storage: {self.__storage}')
+ self.__lock_file.touch()
+
+ if not self.__storage.exists():
+ try:
+ self.__storage.touch()
+ except Exception as err:
+ print(f'Unable to initiate storage: {err}')
+ raise err
+ # prepare an empty cache
+ self.__cache: dict[Any, Any] = {}
+ else:
+ # load a cache from file
+ self.__cache = self.__load_file()
+
+ def __del__(self) -> None:
+ """Dump data to persistent storage and unlock it"""
+ if self.__temporary:
+ return
+ # dump a cache to storage
+ if self.__cache:
+ self.__dump_file()
+ # or remove a file
+ else:
+ self.__storage.unlink()
+ # unlock a storage
+ self.__lock_file.unlink()
+
+ def __check_types(self, data: Any) -> None:
+ """Check if all the data have supported types
+
+ Args:
+ data (Any): object to validate
+
+ Raises:
+ TypeError: If a data type is not supported
+ """
+ if isinstance(data, str | int | float | bool | None):
+ return
+ if isinstance(data, list):
+ for item in data:
+ self.__check_types(item)
+ return
+ if isinstance(data, dict):
+ for item in data.values():
+ self.__check_types(item)
+ return
+ raise TypeError(f'Object type "{type(data)}" is not allowed')
+
+ def __load_file(self) -> dict[Any, Any]:
+ """Read a file to a dictionary
+
+ Returns:
+ dict[Any, Any]: loaded dict object
+ """
+ data: bytes = self.__storage.read_bytes()
+ return json_loads(data)
+
+ def __dump_file(self) -> None:
+ """Dump cache to a file"""
+ data: str = json_dumps(self.__cache)
+ self.__storage.write_text(data)
+
+ def __lock(self) -> None:
+ """Lock storage
+
+ Raises:
+ FileExistsError: Raised if a storage is already locked
+ """
+ if self.__locked:
+ raise FileExistsError(f'Access is already locked: {self.__storage}')
+ self.__locked = True
+
+ def __unlock(self) -> None:
+ """Unlock storage
+
+ Raises:
+ FileNotFoundError: Raised if a storage is already unlocked
+ """
+ if not self.__locked:
+ raise FileNotFoundError(f'Access is already unlocked: {self.__storage}')
+ self.__locked = False
+
+ def __storage_locked(self) -> bool:
+ """Check if a storage is locked
+
+ Returns:
+ bool: Lock status
+ """
+ if self.__lock_file.exists():
+ return True
+ return False
+
+ def delete(self, key: Any = None) -> None:
+ """Delete data from a storage or a full storage
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+ """
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage locked and delete operation cannot be performed: {self.__storage}'
+ )
+ if key:
+ if key not in self.__cache:
+ raise ValueError(
+ f'Object {key} does not exist in storage {self.__storage}'
+ )
+ del self.__cache[key]
+ else:
+ self.__cache = {}
+
+ def write(self, key: Any, value: Any) -> None:
+ # Check types first
+ self.__check_types(key)
+ self.__check_types(value)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and cannot be written: {self.__storage}'
+ )
+ # write a data to a cache
+ self.__lock()
+ self.__cache[key] = value
+ self.__unlock()
+
+ def read(self, key: Any, default: Any = None) -> Any:
+ """Read data from a storage
+
+ Args:
+ key (Any): key name
+ default (Any, optional): Value to return if a key does not exist. Defaults to None.
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+
+ Returns:
+ Any: Value to return
+ """
+ # Check types first
+ self.__check_types(key)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and it is not safe to read: {self.__storage}'
+ )
+ # read a data from cache
+ self.__lock()
+ data: Any | None = self.__cache.get(key, default)
+ self.__unlock()
+
+ return data
diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py
new file mode 100644
index 000000000..ead9f6968
--- /dev/null
+++ b/python/vyos/vpp/control_host.py
@@ -0,0 +1,363 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+from pathlib import Path
+from re import fullmatch as re_fullmatch
+from subprocess import run
+from time import sleep
+
+from vyos.vpp.utils import EthtoolGDrvinfo
+
+
+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 ''.
+ """
+ device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove')
+ if pci_addr:
+ 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}'
+ )
+
+
+def unbind_driver(bus_id: str, device_id: str) -> bool:
+ """Unbind a driver from a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+
+ Returns:
+ bool: True if a driver has been unbound, False otherwise
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ if not Path(f'{device_resolved}/driver').exists():
+ return False
+
+ Path(f'{device_resolved}/driver/unbind').write_text(device_id)
+ return True
+
+
+def probe_driver(bus_id: str, device_id: str) -> None:
+ """Probe driver for a device on a bus
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ """
+ Path(f'/sys/bus/{bus_id}/drivers_probe').write_text(device_id)
+
+
+def load_kernel_module(module_name: str) -> None:
+ """Load a kernel module
+
+ Args:
+ module_name (str): module name
+ """
+ # check if a module already loaded
+ if Path(f'/sys/module/{module_name}').exists():
+ return
+
+ # execute modprobe with the specified module name
+ run(['/usr/sbin/modprobe', '-q', module_name], check=True)
+
+
+def override_driver(bus_id: str, device_id: str, driver_name: str = '') -> None:
+ """Override a driver for a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ driver_name (str, optional): Kernel module (driver) name. Defaults to '' - clear an override.
+
+ Raises:
+ FileNotFoundError: A device does not support driver override
+ ChildProcessError: Failed to override a driver
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ # check if a device supports driver override
+ if not Path(f'{device_resolved}/driver_override').exists():
+ raise FileNotFoundError(f'{device_resolved} does not support driver override')
+
+ if driver_name:
+ load_kernel_module(driver_name)
+
+ unbind_driver(bus_id, device_id)
+
+ # vfio-pci needs special approach
+ if driver_name == 'vfio-pci':
+ vendor: str = Path(f'{device_resolved}/vendor').read_text()
+ device: str = Path(f'{device_resolved}/device').read_text()
+ Path('/sys/module/vfio_pci/drivers/pci:vfio-pci/new_id').write_text(
+ f'{vendor} {device}'
+ )
+
+ # override a driver
+ Path(f'{device_resolved}/driver_override').write_text(f'{driver_name}\n')
+
+ # probe a driver
+ probe_driver(bus_id, device_id)
+
+ # check the result
+ if not Path(f'{device_resolved}/driver').exists():
+ raise ChildProcessError(
+ f'Failed to override a driver to {driver_name} for {bus_id}, {device_id}'
+ )
+
+
+def get_bus_name(iface: str) -> str:
+ """Get bus name
+ Works for PCI, VMbus, maybe something else.
+ Does not work for Virtio and other virtual devices
+ (however, it does not seem we need this for such kind of devices).
+
+ Args:
+ iface (str): interface name
+
+ Returns:
+ str: bus name
+ """
+ device_resolved: Path = Path(f'/sys/class/net/{iface}/device').resolve()
+
+ # Iterate upwards until a `bus` directory is found
+ current_path: Path = device_resolved
+ while True:
+ # Check if a bus info is available
+ subsystem_path = Path(f'{current_path}/subsystem')
+ if subsystem_path.is_symlink():
+ # Read the link to determine the bus type
+ bus_path = subsystem_path.resolve()
+ # Check if the parent directory is a 'bus' directory in '/sys/bus/'
+ if bus_path.parent.name == 'bus':
+ # Return only the last name of the path, e.g., 'pci'
+ return bus_path.name
+
+ # Move up one directory level
+ current_path = current_path.parent
+ if current_path == Path('/sys'):
+ break # Stop if we reach the root of /sys without finding a bus type
+
+ return '' # Return None if no bus type was found
+
+
+def get_eth_name(dev_id: str) -> str:
+ """Find Ethernet interface name by PCI address or UUID
+
+ Args:
+ dev_id (str): PCI address or UUID
+
+ 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():
+ # PCI devices
+ 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})
+ # UUID devices
+ else:
+ try:
+ bus_type: str = get_bus_name(dir.name)
+ iface_addr = EthtoolGDrvinfo(dir.name).bus_info_expand(bus_type)
+ net_devs.update({iface_addr: dir.name})
+ except FileNotFoundError:
+ pass
+
+ # match to provided PCI address or UUID and return a name if found
+ if dev_id in net_devs:
+ return net_devs[dev_id]
+ # raise error if device was not found
+ raise FileNotFoundError(
+ f'A device with ID {dev_id} not found in ethernet interfaces'
+ )
+
+
+def get_dev_id(iface: str) -> str:
+ """Get device ID by its interface name
+
+ Args:
+ iface (str): interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: device ID (PCI address or UUID)
+ """
+ try:
+ # Try to get details via ethtool first
+ ethtool_info = EthtoolGDrvinfo(iface)
+ # For devices represented by UUID we need to expand them
+ # to their full representation
+ if ethtool_info.driver == 'hv_netvsc':
+ return ethtool_info.bus_info_expand('vmbus')
+ return ethtool_info.bus_info
+ except Exception:
+ # raise error if a device ID was not found
+ raise FileNotFoundError(f'Cannot find device ID for interface {iface}')
+
+
+def get_eth_driver(iface: str) -> str:
+ """Find kernel module used for Ethernet interface
+
+ Args:
+ iface (str): Ethernet interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: kernel module name
+ """
+ iface_driver: str = ''
+ driver_dir = Path(f'/sys/class/net/{iface}/device/driver/module')
+ if not driver_dir.exists():
+ # raise error if device was not found
+ raise FileNotFoundError(f'PCI device {iface} not found in ethernet interfaces')
+
+ iface_driver: str = driver_dir.resolve().name
+ return iface_driver
+
+
+def unsafe_noiommu_mode(status: bool) -> None:
+ """Control unsafe_noiommu_mode parameter of vfio module
+
+ Args:
+ status (bool): Target status
+
+ Raises:
+ ChildProcessError: Raised if failed to set unsafe_noiommu_mode
+ """
+ param_path = Path('/sys/module/vfio/parameters/enable_unsafe_noiommu_mode')
+ current_status: str = param_path.read_text().strip()
+ target_status: str = 'Y' if status else 'N'
+ if current_status != target_status:
+ param_path.write_text(target_status)
+ if param_path.read_text().strip() != target_status:
+ raise ChildProcessError('Failed to set unsafe_noiommu_mode')
+
+
+def rename_iface(name_old: str, name_new: str) -> None:
+ """Rename interface
+
+ Args:
+ name_old (str): old name
+ name_new (str): new name
+ """
+ run(['ip', 'link', 'set', name_old, 'down'])
+ rename_cmd: list[str] = ['ip', 'link', 'set', name_old, 'name', name_new]
+ run(rename_cmd)
+
+
+def set_promisc(iface_name: str, operation: str) -> None:
+ """Set promisc mode for interface
+
+ Args:
+ iface_name (str): name of an interface
+ operation (str): operation (on, off)
+ """
+ run(['ip', 'link', 'set', iface_name, 'promisc', operation])
+
+
+def set_mtu(iface_name: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name (str): name of an interface
+ mtu (int): MTU
+ """
+ run(['ip', 'link', 'set', iface_name, 'mtu', str(mtu)])
+
+
+def get_eth_mac(iface_name: str) -> str:
+ """Get MAC address of an interface
+
+ Args:
+ iface_name (str): name of an interface
+
+ Raises:
+ FileNotFoundError: interface was not found
+
+ Returns:
+ str: MAC address
+ """
+ dev_addr_path = Path(f'/sys/class/net/{iface_name}/address')
+ if dev_addr_path.exists():
+ return dev_addr_path.read_text().strip()
+ else:
+ # raise error if device was not found
+ raise FileNotFoundError(f'Interface {iface_name} not found')
+
+
+def xdp_remove(iface_name: str) -> None:
+ """Remove XDP BPF program from an interfce
+
+ Args:
+ iface_name (str): name of an interface
+ """
+ run(['ip', 'link', 'set', iface_name, 'xdp', 'off'])
+
+
+def set_status(iface_name: str, status: str) -> None:
+ """Set interface status
+
+ Args:
+ iface_name (str): name of an interface
+ status (str): status - "up" or "down"
+ """
+ run(['ip', 'link', 'set', iface_name, status])
diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py
new file mode 100644
index 000000000..39ecac906
--- /dev/null
+++ b/python/vyos/vpp/control_vpp.py
@@ -0,0 +1,444 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from collections.abc import Callable
+from functools import wraps
+from re import search as re_search, MULTILINE as re_M
+from systemd import journal
+from time import sleep
+from typing import TypeVar, ParamSpec, Literal
+
+from vpp_papi import VPPApiClient
+from vpp_papi import VPPIOError, VPPValueError
+
+# define types for static type checkers
+AnyType = TypeVar('AnyType')
+AnyParam = ParamSpec('AnyParam')
+
+
+class VPPControl:
+ """Control VPP network stack"""
+
+ class _Decorators:
+ """Decorators for VPPControl"""
+
+ @classmethod
+ def api_call(
+ cls, decorated_func: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """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: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ if not cls.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: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """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: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ 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:
+ error_message = f'VPP API connection timeout: {err}'
+ journal.send(error_message, priority=journal.LOG_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,
+ iface_type: Literal['tun', 'tap', ''] = '',
+ ) -> 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_type (Literal['tun', 'tap', ''], optional): Use explicit interface type in kernel. Defaults to ''.
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, bool | int | str] = {
+ 'is_add': True,
+ 'sw_if_index': iface_index,
+ 'host_if_name': iface_name_kernel,
+ }
+ if iface_type:
+ iface_type_resolve = {'tun': 1, 'tap': 0}
+ api_call_args['host_if_type'] = iface_type_resolve[iface_type]
+ return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2(**api_call_args)
+
+ @_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_v2(
+ is_add=False, sw_if_index=iface_index, host_if_name=iface_name_kernel
+ )
+
+ @_Decorators.api_call
+ def lcp_pair_find(
+ self,
+ kernel_name: str = '',
+ vpp_index_hw: int | None = None,
+ vpp_index_kernel: int | None = None,
+ vpp_name_hw: str = '',
+ vpp_name_kernel: str = '',
+ ) -> dict[str, str | int] | None:
+ """Find LCP pair details
+
+ Args:
+ kernel_name (str, optional): Interface name in the kernel. Defaults to ''.
+ vpp_index_hw (int | None, optional): Interface index in VPP (hardware). Defaults to None.
+ vpp_index_kernel (int | None, optional): Interface index in VPP (kernel). Defaults to None.
+ vpp_name_hw (str, optional): Interface name in VPP (hardware). Defaults to ''.
+ vpp_name_kernel (str, optional): Interface name in VPP (to kernel). Defaults to ''.
+
+ Returns:
+ dict[str, str | int] | None: LCP pair details
+ """
+ filter_dict = {}
+ for filter_name, filter_value in locals().items():
+ if filter_value:
+ filter_dict[filter_name] = filter_value
+
+ # Get list of pairs
+ lcp_pairs = self.lcp_pairs_list()
+
+ # Check each pair
+ for pair in lcp_pairs:
+ pair_found = False
+ # For each item provided in function arguments
+ for filter_name, filter_value in filter_dict.items():
+ # Stop if filter value is not as in a current pair
+ if filter_name in pair and pair[filter_name] != filter_value:
+ pair_found = False
+ break
+ # Set flag to True and check the next filter value
+ pair_found = True
+
+ if pair_found:
+ return pair
+
+ return None
+
+ @_Decorators.api_call
+ def lcp_pairs_list(self) -> list[dict[str, str | int]]:
+ """List all LCP pairs
+
+ Returns:
+ list[dict[str, str | int]]: LCP pairs details
+ """
+ lcp_pairs_details = []
+
+ lcp_pairs = self.__vpp_api_client.api.lcp_itf_pair_get()[1]
+ vpp_ifaces = self.__vpp_api_client.api.sw_interface_dump()
+ for pair in lcp_pairs:
+ pair_details = {
+ 'kernel_name': pair.host_if_name,
+ 'vpp_index_hw': pair.phy_sw_if_index,
+ 'vpp_index_kernel': pair.host_sw_if_index,
+ }
+ for vpp_iface in vpp_ifaces:
+ if vpp_iface.sw_if_index == pair_details['vpp_index_hw']:
+ pair_details['vpp_name_hw'] = vpp_iface.interface_name
+ if vpp_iface.sw_if_index == pair_details['vpp_index_kernel']:
+ pair_details['vpp_name_kernel'] = vpp_iface.interface_name
+
+ lcp_pairs_details.append(pair_details)
+
+ return lcp_pairs_details
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_resync(self) -> None:
+ """Resynchronize objects between kernel and VPP via Netlink
+
+ This clears all routes in VPP configured by LCP and re-creates them
+ based on the current state of the kernel.
+ """
+ return self.__vpp_api_client.api.lcp_resync()
+
+ @_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 match 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
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_create(
+ self,
+ host_if: str,
+ name: str,
+ rxq_num: int = 0,
+ rxq_size: int = 0,
+ txq_size: int = 0,
+ mode: Literal['auto', 'copy', 'zero-copy'] = 'auto',
+ flags: Literal['no_syscall_lock', ''] = '',
+ ) -> None:
+ """Create XDP interface
+
+ Args:
+ host_if (str): name of an interface in kernel
+ name (str): name of an interface in VPP
+ rxq_num (int, optional): Number of receive queues to connect to. Defaults to 0 (all).
+ rxq_size (int, optional): Size of receive queue. Defaults to 0.
+ txq_size (int, optional): Size of tranceive queue. Defaults to 0.
+ mode (Literal['auto', 'copy', 'zero-copy', optional): Zero-copy mode. Defaults to 'auto'.
+ flags (Literal['no_syscall_lock', ''], optional): Syscall lock mode. Defaults to ''.
+ """
+ api_call_args: dict[str, int | str] = {
+ 'host_if': host_if,
+ 'name': name,
+ 'rxq_num': rxq_num,
+ 'rxq_size': rxq_size,
+ 'txq_size': txq_size,
+ }
+ if mode != 'auto':
+ mode_resolve: dict[str, int] = {'auto': 0, 'copy': 1, 'zero-copy': 2}
+ api_call_args['mode'] = mode_resolve[mode]
+ if flags == 'no_systcall_lock':
+ api_call_args['flags'] = 1
+ return self.__vpp_api_client.api.af_xdp_create_v3(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_delete(self, iface_name_vpp: str) -> None:
+ """Delete XDP interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, int] = {'sw_if_index': iface_index}
+ return self.__vpp_api_client.api.af_xdp_delete(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mac(self, iface_name_vpp: str, mac_address: str) -> None:
+ """Set MAC address of an interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mac_address (str): MAC address
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {
+ 'sw_if_index': iface_index,
+ 'mac_address': mac_address,
+ }
+ return self.__vpp_api_client.api.sw_interface_set_mac_address(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mtu(self, iface_name_vpp: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mtu (int): MTU
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {'sw_if_index': iface_index, 'mtu': mtu}
+ return self.__vpp_api_client.api.hw_interface_set_mtu(**api_call_args)
+
+ @_Decorators.api_call
+ def get_sw_if_dev_type(self, ifname: str) -> int | None:
+ """Find interface device type by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ int | None: Interface device type or None (if was not fount)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.interface_dev_type
+ return None
+
+ @property
+ def connected(self) -> bool:
+ """Check if VPP API is connected
+
+ Returns:
+ bool: True if connected, False if not
+ """
+ return self.__vpp_api_client.transport.connected
+
+ @property
+ @_Decorators.api_call
+ def api(self):
+ """Call API
+
+ Returns:
+ Callable[AnyParam, AnyType]: API functions
+ """
+ return self.__vpp_api_client.api
diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py
new file mode 100644
index 000000000..14659ed7b
--- /dev/null
+++ b/python/vyos/vpp/interface/__init__.py
@@ -0,0 +1,40 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from .bond import BondInterface
+from .bridge import BridgeInterface
+from .ethernet import EthernetInterface
+from .geneve import GeneveInterface
+from .gre import GREInterface
+from .ipip import IPIPInterface
+from .loopback import LoopbackInterface
+from .vxlan import VXLANInterface
+from .wireguard import WireguardInterface
+from .xconnect import XconnectInterface
+
+__all__ = [
+ 'BondInterface',
+ 'BridgeInterface',
+ 'EthernetInterface',
+ 'GeneveInterface',
+ 'GREInterface',
+ 'IPIPInterface',
+ 'LoopbackInterface',
+ 'VXLANInterface',
+ 'WireguardInterface',
+ 'XconnectInterface',
+]
diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py
new file mode 100644
index 000000000..1cc306670
--- /dev/null
+++ b/python/vyos/vpp/interface/bond.py
@@ -0,0 +1,113 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.control_host import set_promisc
+
+vpp = VPPControl()
+
+
+class BondInterface:
+ def __init__(
+ self,
+ ifname,
+ mode: str = '',
+ load_balance: int = 0,
+ mac: str = '',
+ kernel_interface: str = '',
+ ):
+ self.instance = int(ifname.removeprefix('bond'))
+ self.ifname = f'BondEthernet{self.instance}'
+ self.mode = mode
+ self.load_balance = load_balance
+ self.mac = mac
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create Bond interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/bonding/bond.api
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.add()
+ """
+ # Create interface 'bondX'
+ create_args = {
+ 'id': self.instance,
+ 'mode': self.mode,
+ 'lb': self.load_balance,
+ }
+ if self.mac:
+ create_args.update({'use_custom_mac': True, 'mac_address': self.mac})
+ vpp.api.bond_create2(**create_args)
+ if self.kernel_interface:
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def delete(self):
+ """Delete Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.delete()
+ """
+ bond_if_index = vpp.get_sw_if_index(self.ifname)
+ vpp.api.bond_delete(sw_if_index=bond_if_index)
+
+ def add_member(self, interface):
+ """Add member to Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.add_member(interface='eth0')
+ """
+ bond_if_index = vpp.get_sw_if_index(f'BondEthernet{self.instance}')
+ member_if_index = vpp.get_sw_if_index(interface)
+ member_if_type = vpp.get_sw_if_dev_type(interface)
+ vpp.api.bond_add_member(
+ bond_sw_if_index=bond_if_index, sw_if_index=member_if_index
+ )
+ vpp.api.sw_interface_set_promisc(sw_if_index=member_if_index, promisc_on=True)
+ if member_if_type == 'AF_XDP interface':
+ set_promisc(f'defunct_{interface}', 'on')
+
+ def detach_member(self, interface):
+ """Detach member from Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.detach_member(interface='eth0')
+ """
+ member_if_index = vpp.get_sw_if_index(interface)
+ vpp.api.bond_detach_member(sw_if_index=member_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py
new file mode 100644
index 000000000..e220547de
--- /dev/null
+++ b/python/vyos/vpp/interface/bridge.py
@@ -0,0 +1,128 @@
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+class BridgeInterface:
+ def __init__(
+ self,
+ ifname: str,
+ flood: bool = True,
+ forward: bool = True,
+ learn: bool = True,
+ uu_flood: bool = True,
+ arp_term: bool = False,
+ ):
+ self.ifname = ifname
+ self.interface_suffix = int(self.ifname.replace('br', ''))
+ self.flood = flood
+ self.forward = forward
+ self.learn = learn
+ self.uu_flood = uu_flood
+ self.arp_term = arp_term
+
+ def add(self):
+ """Create Bridge interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/l2/l2.api
+
+ Bridge-domain 0 is reserved for the default bridge-domain.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add()
+ """
+ vpp.api.bridge_domain_add_del_v2(
+ is_add=True,
+ bd_id=self.interface_suffix,
+ flood=self.flood,
+ forward=self.forward,
+ learn=self.learn,
+ uu_flood=self.uu_flood,
+ arp_term=self.arp_term,
+ )
+
+ def delete(self):
+ """Delete Bridge interface
+
+ Bridge-members must be detached before deleting the bridge interface.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.delete()
+ """
+ vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix)
+
+ def add_member(self, member: str | int):
+ """Add member to Bridge interface
+
+ Attaches a VPP interface to the Bridge interface specified by `interface_suffix`.
+ The `member` parameter can be either the name (str) or the index (int) of the network
+ VPP interface to be added as a member to the bridge.
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be added as a member to the bridge.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add_member(member='eth0')
+ """
+ bridge_index = self.interface_suffix
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = vpp.get_sw_if_index(member)
+
+ return vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=0
+ )
+
+ def detach_member(self, member: str | int):
+ """Detach member from Bridge interface.
+ Bridge-domain 0 is reserved for the default bridge-domain.
+ The `member` parameter can be either the name (str) or the index (int)
+ of the network VPP interface
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be detached from the bridge.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.detach_member(member='eth0')
+ """
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = vpp.get_sw_if_index(member)
+
+ return vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=0, port_type=0
+ )
diff --git a/python/vyos/vpp/interface/ethernet.py b/python/vyos/vpp/interface/ethernet.py
new file mode 100644
index 000000000..ddd3dc42f
--- /dev/null
+++ b/python/vyos/vpp/interface/ethernet.py
@@ -0,0 +1,54 @@
+# VyOS implementation of VPP Ethernet interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+class EthernetInterface:
+ """Interface Ethernet"""
+
+ def __init__(self, ifname, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('eth'))
+ self.ifname = ifname
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ pass
+
+ def delete(self):
+ pass
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py
new file mode 100644
index 000000000..3d5e75003
--- /dev/null
+++ b/python/vyos/vpp/interface/geneve.py
@@ -0,0 +1,90 @@
+# VyOS implementation of Geneve interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+def show():
+ """Show Geneve interface
+ Example:
+ from vyos.vpp.interface import geneve
+ geneve.show()
+ """
+ return vpp.api.geneve_tunnel_dump()
+
+
+class GeneveInterface:
+ def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('geneve'))
+ self.ifname = f'geneve_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create Geneve interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/geneve/geneve.api
+
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='geneve25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.add()
+ """
+ return vpp.api.geneve_add_del_tunnel2(
+ is_add=True,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def delete(self):
+ """Delete Geneve interface
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.delete()
+ """
+ return vpp.api.geneve_add_del_tunnel2(
+ is_add=False,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py
new file mode 100644
index 000000000..0017127ec
--- /dev/null
+++ b/python/vyos/vpp/interface/gre.py
@@ -0,0 +1,85 @@
+# VyOS implementation of VPP GRE interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+def show():
+ """Show GRE interface
+ Example:
+ from vyos.vpp.interface import gre
+ gre.show()
+ """
+ return vpp.api.gre_tunnel_dump()
+
+
+class GREInterface:
+ def __init__(self, ifname, source_address, remote, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('gre'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create GRE interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/gre/gre.api
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.add()
+ """
+ vpp.api.gre_tunnel_add_del(
+ is_add=True,
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ },
+ )
+
+ def delete(self):
+ """Delete GRE interface
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.delete()
+ """
+ return vpp.api.gre_tunnel_add_del(
+ is_add=False, tunnel={'src': self.src_address, 'dst': self.dst_address}
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py
new file mode 100644
index 000000000..e1f19735b
--- /dev/null
+++ b/python/vyos/vpp/interface/ipip.py
@@ -0,0 +1,83 @@
+# VyOS implementation of VPP IPIP interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+def show():
+ """Show IPIP interface
+ Example:
+ from vyos.vpp.interface import ipip
+ ipip.show()
+ """
+ return vpp.api.ipip_tunnel_dump()
+
+
+class IPIPInterface:
+ def __init__(self, ifname, source_address, remote, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('ipip'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create IPIP interface
+ https://github.com/FDio/vpp/blob/stable/2310/src/vnet/ipip/ipip.api
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.add()
+ """
+ vpp.api.ipip_add_tunnel(
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ },
+ )
+
+ def delete(self):
+ """Delete IPIP interface
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.delete()
+ """
+ ipip_if_index = vpp.get_sw_if_index(f'ipip{self.instance}')
+ return vpp.api.ipip_del_tunnel(sw_if_index=ipip_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py
new file mode 100644
index 000000000..df7b82356
--- /dev/null
+++ b/python/vyos/vpp/interface/loopback.py
@@ -0,0 +1,68 @@
+# VyOS implementation of VPP Loopback interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+class LoopbackInterface:
+ """Interface Loopback"""
+
+ def __init__(self, ifname, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('lo'))
+ self.ifname = f'loop{self.instance}'
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create Loopback interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/interface.api
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.add()
+ """
+ vpp.api.create_loopback_instance(is_specified=True, user_instance=self.instance)
+
+ def delete(self):
+ """Delete Loopback interface
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.delete()
+ """
+ loopback_if_index = vpp.get_sw_if_index(f'loop{self.instance}')
+ return vpp.api.delete_loopback(sw_if_index=loopback_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py
new file mode 100644
index 000000000..907fcbee0
--- /dev/null
+++ b/python/vyos/vpp/interface/vxlan.py
@@ -0,0 +1,94 @@
+# VyOS implementation of VPP VXLAN interface
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+def show():
+ """Show VXLAN interface
+ Example:
+ from vyos.vpp.interface import vxlan
+ vxlan.show()
+ """
+ return vpp.api.vxlan_tunnel_dump()
+
+
+class VXLANInterface:
+ """Interface VXLAN"""
+
+ def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('vxlan'))
+ self.ifname = f'vxlan_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create VXLAN interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/vxlan/vxlan.api
+
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.add()
+ """
+ vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=True,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ instance=self.instance,
+ decap_next_index=1,
+ is_l3=False,
+ )
+
+ def delete(self):
+ """Delete VXLAN interface
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.delete()
+ """
+ return vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=False,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ is_l3=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.kernel_add()
+ """
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.kernel_delete()
+ """
+ vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/wireguard.py b/python/vyos/vpp/interface/wireguard.py
new file mode 100644
index 000000000..582929851
--- /dev/null
+++ b/python/vyos/vpp/interface/wireguard.py
@@ -0,0 +1,56 @@
+# VyOS implementation of VPP Wireguard interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+vpp = VPPControl()
+
+
+class WireguardInterface:
+ """Interface Wireguard"""
+
+ def __init__(self, ifname, listen_port=51820, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('wg'))
+ self.ifname = ifname
+ self.listen_port = listen_port
+ self.kernel_interface = kernel_interface
+
+ def add(self):
+ """Create Wireguard interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/wireguard/wireguard.api
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5', listen_port=51820, generate_key=True)
+ a.add()
+ """
+ vpp.api.wireguard_interface_create(
+ generate_key=True,
+ interface={'user_instance': self.instance, 'listen_port': self.listen_port},
+ )
+ if self.kernel_interface:
+ vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def delete(self):
+ """Delete Wireguard interface
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5')
+ a.delete()
+ """
+ wg_if_index = vpp.get_sw_if_index(f'wg{self.instance}')
+ return vpp.api.wireguard_interface_delete(sw_if_index=wg_if_index)
diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py
new file mode 100644
index 000000000..b4bec3321
--- /dev/null
+++ b/python/vyos/vpp/interface/xconnect.py
@@ -0,0 +1,94 @@
+# VyOS implementation of VPP bridge interface
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import iftunnel_transform
+
+vpp = VPPControl()
+
+
+class XconnectInterface:
+ def __init__(
+ self,
+ ifname: str,
+ members: list = [],
+ ):
+ self.ifname = ifname
+ self.members = members
+
+ def add_l2_xconnect(self):
+ """Add l2 cross connect
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.add_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = vpp.get_sw_if_index(first_member)
+ member_second_if_index = vpp.get_sw_if_index(second_member)
+ vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=True,
+ )
+ vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=True,
+ )
+
+ def del_l2_xconnect(self):
+ """Move l2 cross connect member to mode l3 (delte xconnect)
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.del_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = vpp.get_sw_if_index(first_member)
+ member_second_if_index = vpp.get_sw_if_index(second_member)
+ vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=False,
+ )
+ vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=False,
+ )
diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/vpp/nat/__init__.py
diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py
new file mode 100644
index 000000000..08d69bba2
--- /dev/null
+++ b/python/vyos/vpp/utils.py
@@ -0,0 +1,281 @@
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import ctypes
+import socket
+from fcntl import ioctl
+from pathlib import Path
+from struct import pack
+
+
+def iftunnel_transform(iface: str) -> str:
+ """Transform interface name from `xxxNN` to `xxx_tunnelNN`
+
+ Args:
+ iface (str): original interface name
+
+ Raises:
+ ValueError: Raised if an interface name does not start with a alpha and ends with decimal digit
+
+ Returns:
+ str: Transformed interface name
+ """
+ # Check format
+ if not iface[0].isascii() or not iface[-1].isdecimal():
+ raise ValueError(f'Wrong interface name format: {iface}')
+ # Transform
+ iface_type: str = iface.rstrip('0123456789')
+ iface_num: str = iface.removeprefix(iface_type)
+ # Return transformed
+ return f'{iface_type}_tunnel{iface_num}'
+
+
+def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]:
+ """List of all VPP interfaces (CLI names)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[str]: list of interfaces
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ vpp_ifaces: list[str] = []
+
+ # Get a list of Ethernet interfaces
+ for iface in config.get('settings', {}).get('interface', {}).keys():
+ vpp_ifaces.append(iface)
+
+ # Get a list of VPP interfaces
+ for iface_type in config.get('interfaces', {}).keys():
+ for iface in config.get('interfaces', {}).get(iface_type, {}).keys():
+ vpp_ifaces.append(iface)
+
+ return vpp_ifaces
+
+
+def vpp_ifaces_list(vpp_api) -> list[dict]:
+ """List interfaces in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+
+ Returns:
+ list[dict]: list of dictionaries with interfaces
+ """
+ ifaces_list: list[dict] = []
+ sw_ifaces_dump = vpp_api.sw_interface_dump()
+ while sw_ifaces_dump:
+ iface_details = sw_ifaces_dump.pop()
+ ifaces_list.append(iface_details._asdict())
+
+ return ifaces_list
+
+
+def vpp_ip_addresses_by_index(vpp_api, index: str) -> list[str]:
+ """List of IP addresses for interface by its index in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+ index (str): interface index in vpp
+
+ Returns:
+ list[str]: list of IP addresses
+ """
+ ip_addresses_list: list[dict] = []
+ ip_address_dump = vpp_api.ip_address_dump(sw_if_index=index)
+ while ip_address_dump:
+ ip_address_details = ip_address_dump.pop()
+ ip_addresses_list.append(str(ip_address_details._asdict().get('prefix')))
+ return ip_addresses_list
+
+
+def vpp_ifaces_stats(
+ iface_name: str = '',
+) -> dict[str, dict[str, int | dict[str, int]]]:
+ from re import compile as re_compile
+ from vpp_papi import vpp_stats
+
+ def total_value(val_list: vpp_stats.SimpleList) -> int | dict[str, int]:
+ """Helper for aggregation stats from multiple workers
+
+ Args:
+ val_list (vpp_stats.SimpleList): list of stats for all workers
+
+ Returns:
+ int | dict[str, int]: Summary stats
+ """
+ # if all items are int return their sum
+ if all(isinstance(value, int) for value in val_list):
+ return sum(val_list)
+ # if all items are tuple
+ if all(isinstance(value, tuple) for value in val_list):
+ combined_stats = {}
+ # process individual workers
+ for worker_stats in val_list:
+ packets, octets = worker_stats
+ sum_packets = combined_stats.get('packets', 0) + packets
+ sum_octets = combined_stats.get('octets', 0) + octets
+ combined_stats = {'packets': sum_packets, 'bytes': sum_octets}
+ return combined_stats
+
+ # items are something unknown, just return what we received
+ return val_list
+
+ stats = vpp_stats.VPPStats()
+
+ ifaces_stats: dict[str, dict[str, int | dict[str, int]]] = {}
+
+ # prepare parser for stats output
+ regex_parser = re_compile(r'^/interfaces/(?P<iface>[^/]+)/(?P<param>[^/]+)')
+ # get list of available stats and dump them
+ stats_list: list[str] = stats.ls([f'^/interfaces/{iface_name}'])
+ stats_dump: list[dict[str, int]] = stats.dump(stats_list)
+
+ # parse outputs and convert it to a dictionary
+ for stats_key, stats_value in stats_dump.items():
+ parsed_key = regex_parser.search(stats_key).groupdict()
+ iface_name = parsed_key['iface']
+ param = parsed_key['param']
+ stats_item = {param: total_value(stats_value)}
+ if iface_name in ifaces_stats:
+ ifaces_stats[iface_name].update(stats_item)
+ else:
+ ifaces_stats[iface_name] = stats_item
+
+ return ifaces_stats
+
+
+def cli_ifaces_lcp_kernel_list(
+ config_instance, mode: str = 'candidate'
+) -> list[tuple[str, str]]:
+ """List of all VPP kernel-interfaces (CLI names, attached VPP interfaces)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[tuple[str, str]]: list of interfaces ([(vpp_iface, kernel_iface)])
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ lcp_kernel_ifaces: list[tuple[str, str]] = []
+
+ # Get a list with kernel interfaces
+ for ifaces_list in config.get('interfaces', {}).values():
+ for iface_name, iface_settings in ifaces_list.items():
+ if 'kernel_interface' in iface_settings:
+ lcp_kernel_ifaces.append(
+ (iface_name, iface_settings['kernel_interface'])
+ )
+
+ return lcp_kernel_ifaces
+
+
+class EthtoolGDrvinfo:
+ """Return interface details like `ethtol -i` does"""
+
+ # TODO
+ # this probably need to be replaced with a code generator
+ # like ctypeslib or C extension
+ class EthtoolDrvinfo(ctypes.Structure):
+ _fields_ = [
+ ('cmd', ctypes.c_uint32),
+ ('driver', ctypes.c_char * 32), # Driver short name
+ ('version', ctypes.c_char * 32), # Driver version
+ ('fw_version', ctypes.c_char * 32), # Firmware version
+ # Be careful: bus info can be longer than 32 chars and thus truncated
+ ('bus_info', ctypes.c_char * 32), # Bus info.
+ ('erom_version', ctypes.c_char * 32), # Expansion ROM version
+ ('reserved2', ctypes.c_char * 12), # Reserved for future use
+ ('n_priv_flags', ctypes.c_uint32), # Number of private flags
+ ('n_stats', ctypes.c_uint32), # Number of U64 stats
+ ('testinfo_len', ctypes.c_uint32), # Test info length
+ ('eedump_len', ctypes.c_uint32), # EEPROM dump length
+ ('regdump_len', ctypes.c_uint32), # Register dump length
+ ]
+
+ def __init__(self, iface: str):
+ # Constants for ethtool
+ SIOCETHTOOL = 0x8946 # pretend to be ethtool
+ ETHTOOL_GDRVINFO = 0x00000003 # Command to get driver info
+
+ # Create a dummy socket
+ sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # Prepare the request for getting driver info
+ drvinfo = self.EthtoolDrvinfo(cmd=ETHTOOL_GDRVINFO)
+ ifreq: bytes = pack('16sP', iface.encode('utf-8'), ctypes.addressof(drvinfo))
+
+ # Make an ioctl call to get the driver info
+ try:
+ ioctl(sockfd, SIOCETHTOOL, ifreq)
+ except OSError:
+ raise FileNotFoundError(f'There is no Ethernet device: {iface}')
+
+ # Close the socket
+ sockfd.close()
+
+ # save the information
+ self.driver: str = drvinfo.driver.decode('utf-8').strip('\x00')
+ self.version: str = drvinfo.version.decode('utf-8').strip('\x00')
+ self.fw_version: str = drvinfo.fw_version.decode('utf-8').strip('\x00')
+ self.bus_info: str = drvinfo.bus_info.decode('utf-8').strip('\x00')
+ self.erom_version: str = drvinfo.erom_version.decode('utf-8').strip('\x00')
+ self.reserved2: str = drvinfo.reserved2.decode('utf-8').strip('\x00')
+ self.n_priv_flags: int = drvinfo.n_priv_flags
+ self.testinfo_len: int = drvinfo.testinfo_len
+ self.eedump_len: int = drvinfo.eedump_len
+ self.regdump_len: int = drvinfo.regdump_len
+
+ def bus_info_expand(self, bus_name: str) -> str:
+ bus_path = Path(f'/sys/bus/{bus_name}/devices').glob(f'{self.bus_info}*')
+ dev_ids = list(bus_path)
+ if not dev_ids:
+ raise FileNotFoundError(
+ f'No matching IDs on the bus: {self.bus_info} on {bus_name}'
+ )
+ if len(dev_ids) > 1:
+ raise FileNotFoundError(
+ f'There are more than one matching IDs on the bus: {dev_ids} on {bus_name}'
+ )
+ return dev_ids[0].name
diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py
new file mode 100755
index 000000000..ed9e4f942
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpp.py
@@ -0,0 +1,914 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import os
+import re
+import unittest
+
+from json import loads
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.utils.process import rc_cmd
+
+PROCESS_NAME = 'vpp_main'
+VPP_CONF = '/run/vpp/vpp.conf'
+base_path = ['vpp']
+driver = 'dpdk'
+interface = 'eth1'
+
+
+def get_address(interface):
+ rc, data = rc_cmd(f'ip --json address show dev {interface}')
+ if rc == 0:
+ data = loads(data)
+ if isinstance(data, list) and len(data) > 0:
+ ip_address = data[0]['addr_info'][0]['local']
+ return ip_address
+
+
+class TestVPP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestVPP, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def setUp(self):
+ self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver])
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10'])
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete test config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(os.path.exists(VPP_CONF))
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_vpp_basic(self):
+ main_core = '0'
+ poll_sleep = '0'
+
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core])
+ self.cli_set(base_path + ['settings', 'lcp', 'route-no-paths'])
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', poll_sleep])
+
+ # commit changes
+ self.cli_commit()
+
+ config_entries = (
+ f'poll-sleep-usec {poll_sleep}',
+ f'main-core {main_core}',
+ 'plugin default { disable }',
+ 'plugin dpdk_plugin.so { enable }',
+ 'plugin linux_cp_plugin.so { enable }',
+ 'dev 0000:00:00.0',
+ 'uio-bind-force',
+ )
+
+ # Check configured options
+ config = read_file(VPP_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ # route-no-paths is not present in the output
+ # looks like vpp bug
+ _, out = rc_cmd('sudo vppctl show lcp')
+ required_str = 'route-no-paths'
+ self.assertIn(required_str, out)
+
+ def test_02_vpp_vxlan(self):
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ interface_kernel = f'vpptap{vni}'
+ new_interface_kernel = f'vpptap1{vni}'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.3'
+ remote_address = '192.0.2.254'
+ kernel_address = '203.0.113.1'
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/24']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check vxlan interface
+ _, out = rc_cmd('sudo vppctl show vxlan tunnel')
+ required_str = f'[0] instance 23 src {source_address} dst {remote_address} src_port 4789 dst_port 4789 vni {vni}'
+ self.assertIn(required_str, out)
+
+ # update vxlan interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+ self.cli_commit()
+
+ # check gre interface after update
+ _, out = rc_cmd('sudo vppctl show vxlan tunnel')
+ required_str = (
+ f'[0] instance {vni} src {new_source_address} dst {remote_address}'
+ )
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # change vpp settings
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '5'])
+ self.cli_commit()
+
+ config = read_file(VPP_CONF)
+ self.assertIn('poll-sleep-usec 5', config)
+
+ # delete vxlan kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update vxlan kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete vxlan kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'vxlan', interface_vxlan, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete vxlan interface
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan])
+ self.cli_commit()
+
+ def test_03_vpp_gre(self):
+ interface_gre = 'gre12'
+ interface_kernel = 'vpptun12'
+ new_interface_kernel = 'vpptun123'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '192.0.2.254'
+ kernel_address = '10.0.0.0'
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'gre', interface_gre, 'remote', remote_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check gre interface
+ _, out = rc_cmd('sudo vppctl show gre tunnel')
+ required_str = f'[0] instance 12 src {source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+
+ # update gre interface
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'source-address', new_source_address]
+ )
+ self.cli_commit()
+
+ # check gre interface after update
+ _, out = rc_cmd('sudo vppctl show gre tunnel')
+ required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete gre kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update gre kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'gre',
+ interface_gre,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete kernel interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete gre kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'gre', interface_gre, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete gre interface
+ self.cli_set(base_path + ['interfaces', 'gre', interface_gre])
+ self.cli_commit()
+
+ def test_04_vpp_geneve(self):
+ vni = '2'
+ # Must be 'geneve0' to pass smoketest
+ # As geneve interfaces cannot be named with "instance" suffix
+ interface_geneve = 'geneve0'
+ interface_kernel = f'vpptun{vni}'
+ new_interface_kernel = f'vpptun1{vni}'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '203.0.113.10'
+ kernel_address = '10.0.0.1'
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'source-address',
+ source_address,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'geneve', interface_geneve, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve, 'vni', vni])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check geneve interface
+ _, out = rc_cmd('sudo vppctl show geneve tunnel')
+ required_str = f'[0] lcl {source_address} rmt {remote_address} vni {vni}'
+ self.assertIn(required_str, out)
+
+ # update geneve interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+ self.cli_commit()
+
+ # check geneve interface after update
+ _, out = rc_cmd('sudo vppctl show geneve tunnel')
+ required_str = f'[0] lcl {new_source_address} rmt {remote_address} vni {vni}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete geneve kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update gemeve kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete geneve kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'geneve', interface_geneve, 'kernel-interface']
+ )
+ self.cli_commit()
+
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete geneve interface
+ self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve])
+ self.cli_commit()
+
+ def test_05_vpp_loopback(self):
+ interface_loopback = 'lo11'
+ interface_kernel = 'vpptun11'
+ new_interface_kernel = 'vpptun12'
+ kernel_address = '192.0.2.54'
+
+ self.cli_set(base_path + ['interfaces', 'loopback', interface_loopback])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/25']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check loopback interface
+ _, out = rc_cmd('sudo vppctl show interface loop11')
+ required_str = 'loop11'
+ self.assertIn(required_str, out)
+
+ # delete loopback kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update loopback kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete loopback kernel-interface
+ self.cli_delete(
+ base_path
+ + ['interfaces', 'loopback', interface_loopback, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete loopback interface
+ self.cli_set(base_path + ['interfaces', 'loopback', interface_loopback])
+ self.cli_commit()
+
+ def test_06_vpp_bonding(self):
+ interface_bond = 'bond23'
+ interface_kernel = 'vpptun23'
+ hash = 'layer3+4'
+ mode = '802.3ad'
+ description = 'Interface-Bonding'
+ vlans = ['123', '456']
+ vlan_description = 'My-vlan-123'
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bonding',
+ interface_bond,
+ 'member',
+ 'interface',
+ interface,
+ ]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'bonding', interface_bond, 'hash-policy', hash]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'bonding', interface_bond, 'mode', mode]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bonding',
+ interface_bond,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'description', description]
+ )
+ for vlan in vlans:
+ self.cli_set(
+ base_path
+ + [
+ 'kernel-interfaces',
+ interface_kernel,
+ 'vif',
+ vlan,
+ 'description',
+ vlan_description,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}'))
+
+ current_alias = read_file(f'/sys/class/net/{interface_kernel}/ifalias')
+ vlan_alias = read_file(f'/sys/class/net/{interface_kernel}.{vlan}/ifalias')
+ self.assertEqual(current_alias, description)
+ self.assertEqual(vlan_alias, vlan_description)
+
+ # check bonding interface
+ _, out = rc_cmd('sudo vppctl show bond details')
+ required_enries = (
+ 'BondEthernet23',
+ 'mode: lacp',
+ 'load balance: l34',
+ 'number of active members: 0',
+ 'number of members: 1',
+ f'{interface}',
+ 'device instance: 0',
+ 'interface id: 23',
+ )
+ for entry in required_enries:
+ self.assertIn(entry, out)
+
+ # delete vpp kernel-interface vlan
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel, 'vif'])
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}'))
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete bonding kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'bonding', interface_bond, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete bonding interface
+ self.cli_set(base_path + ['interfaces', 'bonding', interface_bond])
+ self.cli_commit()
+
+ def test_07_vpp_bridge(self):
+ fake_member = 'eth2'
+ members = [interface]
+ interface_bridge = 'br10'
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ source_address = '192.0.2.1'
+ remote_address = '192.0.2.254'
+
+ for member in members:
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ member,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+
+ # Perform assertions based on the normalized output
+ self.assertIn('BD-ID Index BSN Age(min)', normalized_out)
+ self.assertIn('10 1 0 off', normalized_out)
+ self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out)
+ self.assertIn('on on flood on', normalized_out)
+ self.assertIn('Interface If-idx ISN', normalized_out)
+ # Check Interface, If-idx, ISN
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+
+ # Set non exist member
+ # expect raise ConfigErro
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ fake_member,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ fake_member,
+ ]
+ )
+
+ # Add VXLAN to the bridge
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ interface_vxlan,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+
+ # Perform assertions based on the normalized output
+ self.assertIn('BD-ID Index BSN Age(min)', normalized_out)
+ self.assertIn('10 1 1 off', normalized_out)
+ self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out)
+ self.assertIn('on on flood on', normalized_out)
+ self.assertIn('Interface If-idx ISN', normalized_out)
+ # Check Interface, If-idx, ISN
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+ self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+')
+
+ # Add check dependency ethernet => bridge
+ self.cli_set(
+ base_path + ['settings', 'interface', interface, 'dpdk-options', 'promisc']
+ )
+ self.cli_commit()
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+ self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+')
+
+ def test_08_vpp_ipip(self):
+ interface_ipip = 'ipip12'
+ interface_kernel = 'vpptun12'
+ new_interface_kernel = 'vpptun123'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '192.0.2.5'
+ kernel_address = '10.0.0.0'
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'ipip', interface_ipip, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'ipip', interface_ipip, 'remote', remote_address]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check ipip interface
+ _, out = rc_cmd('sudo vppctl show ipip tunnel')
+ required_str = f'[0] instance 12 src {source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+
+ # update ipip interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+ self.cli_commit()
+
+ # check ipip interface after update
+ _, out = rc_cmd('sudo vppctl show ipip tunnel')
+ required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete ipip kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update ipip kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete kernel interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete ipip kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'ipip', interface_ipip, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete ipip interface
+ self.cli_set(base_path + ['interfaces', 'ipip', interface_ipip])
+ self.cli_commit()
+
+ def test_09_vpp_xconnect(self):
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ interface_xconnect = f'xcon{vni}'
+ source_address = '192.0.2.1'
+ remote_address = '192.0.2.254'
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+
+ # Add xconneect
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'xconnect',
+ interface_xconnect,
+ 'member',
+ 'interface',
+ interface,
+ ]
+ )
+
+ # Cross connect interfaces require 2 interfaces
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'xconnect',
+ interface_xconnect,
+ 'member',
+ 'interface',
+ interface_vxlan,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check interface mode
+ _, out = rc_cmd('sudo vppctl show mode')
+ required_str_list = [
+ f'l2 xconnect {interface} vxlan_tunnel{vni}',
+ f'l2 xconnect vxlan_tunnel{vni} {interface}',
+ ]
+ for required_string in required_str_list:
+ self.assertIn(required_string, out)
+
+ # delete xconnect interface
+ self.cli_delete(base_path + ['interfaces', 'xconnect', interface_xconnect])
+ self.cli_commit()
+
+ # check delete xconnect interface
+ _, out = rc_cmd('sudo vppctl show mode')
+ for required_string in required_str_list:
+ self.assertNotIn(required_string, out)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/completion/list_vpp_interfaces.py b/src/completion/list_vpp_interfaces.py
new file mode 100644
index 000000000..ea6a77bed
--- /dev/null
+++ b/src/completion/list_vpp_interfaces.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS Inc.
+#
+# 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 vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import vpp_ifaces_list
+
+
+def get_vpp_ifaces_names():
+ config = ConfigTreeQuery()
+ if not config.exists('vpp settings interface'):
+ return []
+
+ vpp = VPPControl()
+ vpp_ifaces = vpp_ifaces_list(vpp.api)
+ ifaces_names = [iface['interface_name'] for iface in vpp_ifaces]
+
+ return sorted(ifaces_names)
+
+
+if __name__ == "__main__":
+ ifaces = []
+ ifaces = get_vpp_ifaces_names()
+ print(" ".join(ifaces))
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
new file mode 100755
index 000000000..32dd0d01a
--- /dev/null
+++ b/src/conf_mode/vpp.py
@@ -0,0 +1,553 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from pathlib import Path
+
+from psutil import virtual_memory
+from pyroute2 import IPRoute
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
+from vyos.configdict import node_changed, leaf_node_changed
+from vyos.utils.cpu import get_core_count
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.process import call
+from vyos.utils.system import sysctl_read, sysctl_apply
+
+from vyos.vpp import VPPControl
+from vyos.vpp import control_host
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import verify_dev_driver
+from vyos.vpp.config_filter import iface_filter_eth
+from vyos.vpp.utils import EthtoolGDrvinfo
+from vyos.vpp.configdb import JSONStorage
+
+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'
+
+dependency_interface_type_map = {
+ 'vpp_interfaces_bonding': 'bonding',
+ 'vpp_interfaces_bridge': 'bridge',
+ 'vpp_interfaces_ethernet': 'ethernet',
+ 'vpp_interfaces_geneve': 'geneve',
+ 'vpp_interfaces_gre': 'gre',
+ 'vpp_interfaces_ipip': 'ipip',
+ 'vpp_interfaces_loopback': 'loopback',
+ 'vpp_interfaces_vxlan': 'vxlan',
+ 'vpp_interfaces_xconnect': 'xconnect',
+}
+
+# dict of drivers that needs to be overrided
+override_drivers: dict[str, str] = {
+ 'hv_netvsc': 'uio_hv_generic',
+ 'ena': 'vfio-pci',
+}
+
+# drivers that does not use PCIe addresses
+not_pci_drv: list[str] = ['hv_netvsc']
+
+
+def get_config(config=None):
+ # use persistent config to store interfaces data between executions
+ # this is required because some interfaces after they are connected
+ # to VPP is really hard or impossible to restore without knowing
+ # their original parameters (like IDs)
+ persist_config = JSONStorage('vpp_conf')
+ eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read(
+ 'eth_ifaces', {}
+ )
+
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp']
+ base_settings = ['vpp', 'settings']
+
+ # find interfaces removed from VPP
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ xconn_members = deps_xconnect_dict(conf)
+
+ removed_ifaces = []
+ tmp = node_changed(conf, base_settings + ['interface'])
+ if tmp:
+ for removed_iface in tmp:
+ to_append = {
+ 'iface_name': removed_iface,
+ 'driver': effective_config['settings']['interface'][removed_iface][
+ 'driver'
+ ],
+ }
+ removed_ifaces.append(to_append)
+ # add an interface to a list of interfaces that need
+ # to be reinitialized after the commit
+ set_dependents('ethernet', conf, removed_iface)
+
+ if not conf.exists(base):
+ return {
+ 'removed_ifaces': removed_ifaces,
+ 'xconn_members': xconn_members,
+ 'persist_config': eth_ifaces_persist,
+ }
+
+ config = conf.get_config_dict(
+ base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ # add running config
+ if effective_config:
+ config['effective'] = effective_config
+
+ if 'settings' in config:
+ if 'interface' in config['settings']:
+ for iface, iface_config in config['settings']['interface'].items():
+ # Driver must be configured to continue
+ if 'driver' not in iface_config:
+ raise ConfigError(
+ f'"driver" must be configured for {iface} interface!'
+ )
+
+ old_driver = leaf_node_changed(
+ conf, base_settings + ['interface', iface, 'driver']
+ )
+
+ # Get current kernel module, required for extra verification and
+ # logic for VMBus interfaces
+ config['settings']['interface'][iface]['kernel_module'] = (
+ EthtoolGDrvinfo(iface).driver
+ )
+
+ # filter unsupported config nodes
+ iface_filter_eth(conf, iface)
+ set_dependents('ethernet', conf, iface)
+ # Interfaces with changed driver should be removed/readded
+ if old_driver and old_driver[0] == 'dpdk':
+ removed_ifaces.append(
+ {
+ 'iface_name': iface,
+ 'driver': 'dpdk',
+ }
+ )
+
+ # Get PCI address or device ID
+ if iface_config['driver'] == 'dpdk':
+ if 'dpdk_options' not in iface_config:
+ iface_config['dpdk_options'] = {}
+ # Check in a persistent config first
+ id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get(
+ 'dev_id'
+ )
+ if id_from_persisten_conf:
+ iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf
+ else:
+ try:
+ iface_to_search = iface
+ if old_driver and old_driver[0] == 'xdp':
+ iface_to_search = f'defunct_{iface}'
+ iface_config['dpdk_options']['dev_id'] = (
+ control_host.get_dev_id(iface_to_search)
+ )
+ except Exception:
+ # Return empty address if all attempts failed
+ # We will catch this in verify()
+ iface_config['dpdk_options']['dev_id'] = ''
+ # prepare XDP interface parameters
+ if iface_config['driver'] == 'xdp':
+ xdp_api_params = {
+ 'rxq_size': int(iface_config['xdp_options']['rx_queue_size']),
+ 'txq_size': int(iface_config['xdp_options']['tx_queue_size']),
+ }
+ if iface_config['xdp_options']['num_rx_queues'] == 'all':
+ xdp_api_params['rxq_num'] = 0
+ else:
+ xdp_api_params['rxq_num'] = int(
+ iface_config['xdp_options']['num_rx_queues']
+ )
+ if 'zero-copy' in iface_config['xdp_options']:
+ xdp_api_params['mode'] = 'zero-copy'
+ if 'zero-copy' in iface_config['xdp_options']:
+ xdp_api_params['flags'] = 'no_syscall_lock'
+ iface_config['xdp_api_params'] = xdp_api_params
+
+ if removed_ifaces:
+ config['removed_ifaces'] = removed_ifaces
+ config['xconn_members'] = xconn_members
+
+ # Dependencies
+ for dependency, interface_type in dependency_interface_type_map.items():
+ # if conf.exists(base + ['interfaces', interface_type]):
+ if effective_config.get('interfaces', {}).get(interface_type):
+ for iface, iface_config in (
+ config.get('interfaces', {}).get(interface_type, {}).items()
+ ):
+ # filter unsupported config nodes
+ if interface_type == 'ethernet':
+ iface_filter_eth(conf, iface)
+ set_dependents(dependency, conf, iface)
+
+ # Save important info about all interfaces that cannot be retrieved later
+ # Add new interfaces (only if they are first time seen in a config)
+ for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
+ if iface not in effective_config.get('settings', {}).get('interface', {}):
+ eth_ifaces_persist[iface] = {
+ 'original_driver': config['settings']['interface'][iface][
+ 'kernel_module'
+ ],
+ }
+ eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface)
+ eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface)
+
+ # Return to config dictionary
+ config['persist_config'] = eth_ifaces_persist
+
+ return config
+
+
+def convert_to_int(val):
+ rates = {
+ 'K': 1024,
+ 'M': 1024**2,
+ 'G': 1024**3,
+ }
+ try:
+ return int(val)
+ except ValueError:
+ return int(val[:-1]) * rates[val[-1]]
+
+
+def verify_memory(settings):
+ memory_available: int = virtual_memory().available
+ cpus: int = get_core_count()
+
+ nr_hugepages = int(settings['host_resources']['nr_hugepages'])
+ hugepages_memory = nr_hugepages * 2 * 1024**2
+ memory_required = hugepages_memory
+
+ buffers_per_numa = int(settings.get('buffers', {}).get('buffers_per_numa', 16384))
+ data_size = int(settings.get('buffers', {}).get('data_size', 2048))
+ buffers_memory = buffers_per_numa * data_size * cpus
+
+ memory_required += buffers_memory
+
+ netlink_buffer_size = int(
+ settings.get('lcp', {}).get('netlink', {}).get('rx_buffer_size', 212992)
+ )
+ memory_required += netlink_buffer_size
+
+ memory_main_heap = convert_to_int(
+ settings.get('memory', {}).get('main_heap_size', '1G')
+ )
+ memory_required += memory_main_heap
+
+ statseg_size = convert_to_int(settings.get('statseg', {}).get('size', '96M'))
+ memory_required += statseg_size
+
+ if memory_available < memory_required:
+ raise ConfigError(
+ 'Not enough free memory to start VPP:\n'
+ f'available: {round(memory_available / 1024 ** 3, 1)}GB\n'
+ f'required: {round(memory_required / 1024 ** 3, 1)}GB'
+ )
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ return None
+
+ if 'settings' not in config:
+ raise ConfigError('"settings interface" is required but not set!')
+
+ # CPU main-core must be not included to corelist-workers
+ if config.get('settings').get('cpu', {}).get('main_core') and config.get(
+ 'settings'
+ ).get('cpu', {}).get('corelist_workers'):
+ corelist_workers = config['settings']['cpu']['corelist_workers']
+ main_core = int(config['settings']['cpu']['main_core'])
+
+ all_core_numbers = []
+ for worker_range in corelist_workers:
+ core_numbers = worker_range.split('-')
+ all_core_numbers.extend(
+ range(int(core_numbers[0]), int(core_numbers[-1]) + 1)
+ )
+
+ if main_core in all_core_numbers:
+ raise ConfigError(
+ f'"cpu main-core {main_core}" must not be included in the corelist-workers!'
+ )
+
+ if 'interface' not in config['settings']:
+ raise ConfigError('"settings interface" is required but not set!')
+
+ # check if Ethernet interfaces exist
+ ethernet_ifaces = Section.interfaces('ethernet')
+ for iface in config['settings']['interface'].keys():
+ if iface not in ethernet_ifaces:
+ raise ConfigError(f'Interface {iface} does not exist or is not Ethernet!')
+
+ # ensure DPDK/XDP settings are properly configured
+ for iface, iface_config in config['settings']['interface'].items():
+ # check if selected driver is supported, but only for new interfaces
+ if iface not in config.get('effective', {}).get('settings', {}).get(
+ 'interface', {}
+ ):
+ if not verify_dev_driver(iface, iface_config['driver']):
+ raise ConfigError(
+ f'Driver {iface_config["driver"]} is not compatible with interface {iface}!'
+ )
+ if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config:
+ if iface_config['xdp_options']['num_rx_queues'] != 'all':
+ Warning(f'Not all RX queues will be connected to VPP for {iface}!')
+
+ if 'cpu' in config['settings']:
+ if (
+ 'corelist_workers' in config['settings']['cpu']
+ and 'main_core' not in config['settings']['cpu']
+ ):
+ raise ConfigError('"cpu main-core" is required but not set!')
+
+ verify_memory(config['settings'])
+
+ # Check if deleted interfaces are not xconnect memebrs
+ for iface_config in config.get('removed_ifaces', []):
+ if iface_config['iface_name'] in config.get('xconn_members', {}):
+ raise ConfigError(
+ f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed'
+ )
+
+
+def generate(config):
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ render(service_conf, 'vpp/startup.conf.j2', config['settings'])
+ render(systemd_override, 'vpp/override.conf.j2', config)
+
+ # apply sysctl values
+ # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf
+ sysctl_config: dict[str, str] = {
+ 'vm.nr_hugepages': config['settings']['host_resources']['nr_hugepages'],
+ 'vm.max_map_count': config['settings']['host_resources']['max_map_count'],
+ 'vm.hugetlb_shm_group': '0',
+ 'kernel.shmmax': config['settings']['host_resources']['shmmax'],
+ }
+ # we do not want to lower current values
+ for sysctl_key, sysctl_value in sysctl_config.items():
+ # perform check only for quantitative params
+ if sysctl_key == 'vm.hugetlb_shm_group':
+ pass
+ current_value = sysctl_read(sysctl_key)
+ if int(current_value) > int(sysctl_value):
+ sysctl_config[sysctl_key] = current_value
+
+ if not sysctl_apply(sysctl_config):
+ raise ConfigError('Cannot configure sysctl parameters for VPP')
+
+ return None
+
+
+def apply(config):
+ # Open persistent config
+ # It is required for operations with interfaces
+ persist_config = JSONStorage('vpp_conf')
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ # Cleanup persistent config
+ persist_config.delete()
+ # And stop the service
+ call(f'systemctl stop {service_name}.service')
+ else:
+ # Some interfaces required extra preparation before VPP can be started
+ if 'settings' in config and 'interface' in config.get('settings'):
+ for iface, iface_config in config['settings']['interface'].items():
+ if iface_config['driver'] == 'dpdk':
+ # ena interfaces require noiommu mode
+ if iface_config['kernel_module'] == 'ena':
+ control_host.unsafe_noiommu_mode(True)
+
+ if iface_config['kernel_module'] in override_drivers:
+ control_host.override_driver(
+ config['persist_config'][iface]['bus_id'],
+ config['persist_config'][iface]['dev_id'],
+ override_drivers[iface_config['kernel_module']],
+ )
+
+ call('systemctl daemon-reload')
+ call(f'systemctl restart {service_name}.service')
+
+ # Initialize interfaces removed from VPP
+ for iface in config.get('removed_ifaces', []):
+ # DPDK - rescan PCI to use a proper driver
+ if (
+ iface['driver'] == 'dpdk'
+ and config['persist_config'][iface['iface_name']]['original_driver']
+ not in not_pci_drv
+ ):
+ control_host.pci_rescan(
+ config['persist_config'][iface['iface_name']]['dev_id']
+ )
+ # rename to the proper name
+ iface_new_name: str = control_host.get_eth_name(
+ config['persist_config'][iface['iface_name']]['dev_id']
+ )
+ control_host.rename_iface(iface_new_name, iface['iface_name'])
+ # XDP - rename an interface , disable promisc and XDP
+ if iface['driver'] == 'xdp':
+ control_host.set_promisc(f'defunct_{iface["iface_name"]}', 'off')
+ control_host.rename_iface(
+ f'defunct_{iface["iface_name"]}', iface['iface_name']
+ )
+ control_host.xdp_remove(iface['iface_name'])
+ # Rename Mellanox NIC to a normal name
+ try:
+ if (
+ control_host.get_eth_driver(f'defunct_{iface["iface_name"]}')
+ == 'mlx5_core'
+ ):
+ control_host.rename_iface(
+ f'defunct_{iface["iface_name"]}', iface['iface_name']
+ )
+ except FileNotFoundError:
+ pass
+ # Replace a driver with original for VMBus interfaces and rename it
+ if (
+ iface['driver'] == 'dpdk'
+ and config['persist_config'][iface['iface_name']]['original_driver']
+ in override_drivers
+ ):
+ control_host.override_driver(
+ config['persist_config'][iface['iface_name']]['bus_id'],
+ config['persist_config'][iface['iface_name']]['dev_id'],
+ )
+ iface_new_name: str = control_host.get_eth_name(
+ config['persist_config'][iface['iface_name']]['dev_id']
+ )
+ control_host.rename_iface(iface_new_name, iface['iface_name'])
+
+ # Remove what is not in the config anymore
+ if iface['iface_name'] not in config.get('settings', {}).get('interface', {}):
+ del config['persist_config'][iface['iface_name']]
+
+ if 'settings' in config and 'interface' in config.get('settings'):
+ # 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)
+ # preconfigure LCP plugin
+ if 'route_no_paths' in config.get('settings', {}).get('lcp', {}):
+ vpp_control.cli_cmd('lcp param route-no-paths on')
+ else:
+ vpp_control.cli_cmd('lcp param route-no-paths off')
+ # add interfaces
+ iproute = IPRoute()
+ for iface, iface_config in config['settings']['interface'].items():
+ # promisc option for DPDK interfaces
+ if iface_config['driver'] == 'dpdk':
+ if 'promisc' in iface_config['dpdk_options']:
+ if_index = vpp_control.get_sw_if_index(iface)
+ vpp_control.api.sw_interface_set_promisc(
+ sw_if_index=if_index, promisc_on=True
+ )
+ # add XDP interfaces
+ if iface_config['driver'] == 'xdp':
+ control_host.rename_iface(iface, f'defunct_{iface}')
+ vpp_control.xdp_iface_create(
+ host_if=f'defunct_{iface}',
+ name=iface,
+ **iface_config['xdp_api_params'],
+ )
+ # replicate MAC address of a real interface
+ real_mac = control_host.get_eth_mac(f'defunct_{iface}')
+ vpp_control.set_iface_mac(iface, real_mac)
+ if 'promisc' in iface_config['xdp_options']:
+ control_host.set_promisc(f'defunct_{iface}', 'on')
+ control_host.set_status(f'defunct_{iface}', 'up')
+ # Rename Mellanox interfaces to hide them and create LCP properly
+ if (
+ iface in Section.interfaces()
+ and control_host.get_eth_driver(iface) == 'mlx5_core'
+ ):
+ control_host.rename_iface(iface, f'defunct_{iface}')
+ control_host.set_status(f'defunct_{iface}', 'up')
+ # Create lcp
+ if iface not in Section.interfaces():
+ vpp_control.lcp_pair_add(iface, iface)
+
+ # Set rx-mode
+ rx_mode = iface_config.get('rx_mode')
+ if rx_mode:
+ # to hardware side
+ vpp_control.iface_rxmode(iface, rx_mode)
+ # to kernel side
+ lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get(
+ 'vpp_name_kernel'
+ )
+ vpp_control.iface_rxmode(lcp_name, rx_mode)
+
+ # For unknown reasons, if multiple interfaces later try to be
+ # initialized by configuration scripts, some of them may stuck
+ # in an endless UP/DOWN loop
+ # We found two workarounds - pause initialization (requires
+ # main code modifications).
+ # And this one
+ dev_index = iproute.link_lookup(ifname=iface)[0]
+ iproute.link('set', index=dev_index, state='up')
+
+ # Syncronize routes via LCP
+ vpp_control.lcp_resync()
+
+ # Save persistent config
+ if 'persist_config' in config and config['persist_config']:
+ persist_config.write('eth_ifaces', config['persist_config'])
+
+ # reinitialize interfaces, but not during the first boot
+ if boot_configuration_complete():
+ call_dependents()
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py
new file mode 100644
index 000000000..9b057f992
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bonding.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos import ConfigError
+
+from vyos.vpp.interface import BondInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_list, cli_ifaces_lcp_kernel_list
+
+
+def _get_bond_mode(mode_name: str) -> int:
+ """Convert VyOS CLI name bonding mode to VPP compatible"""
+ mode_mapping = {
+ 'round-robin': 1,
+ 'active-backup': 2,
+ 'xor-hash': 3,
+ 'broadcast': 4,
+ '802.3ad': 5,
+ }
+
+ return mode_mapping.get(mode_name, 5)
+
+
+def _get_bond_lb(lb_name: str) -> int:
+ """Convert VyOS CLI name bonding load balance to VPP compatible"""
+ lb_mapping = {
+ 'layer2': 0,
+ 'layer2+3': 2,
+ 'layer3+4': 1,
+ }
+
+ return lb_mapping.get(lb_name, 5)
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Bonding interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Bonding interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'bonding']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ if __name__ == '__main__':
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ # if a file was started as dependency, we are starting from empty config
+ else:
+ effective_config = {}
+
+ if not config:
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate')
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # convert values to VPP compatible
+ if 'mode' in config:
+ config['mode'] = _get_bond_mode(config['mode'])
+ if 'hash_policy' in config:
+ config['hash_policy'] = _get_bond_lb(config['hash_policy'])
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ if effective_config.get('kernel_interface') or __name__ != '__main__':
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ verify_vpp_remove_kernel_interface(config)
+ verify_vpp_remove_xconnect_interface(config)
+
+ # Member must belong to VPP
+ for iface in config.get('member', {}).get('interface', []):
+ if iface not in config['vpp_ifaces']:
+ raise ConfigError(f'{iface} must be a VPP interface for bonding')
+
+ if 'remove' in config:
+ return None
+
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # remove old members
+ if 'effective' in config:
+ members = config['effective'].get('member', {}).get('interface', [])
+
+ kernel_interface = config['effective'].get('kernel_interface', '')
+ i = BondInterface(ifname, kernel_interface=kernel_interface)
+ for member in members:
+ i.detach_member(interface=member)
+
+ if 'kernel_interface' in config['effective']:
+ i.kernel_delete()
+ # Delete bonding interface
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Create a new one
+ mode = config.get('mode')
+ lb = config.get('hash_policy')
+ members = config.get('member', {}).get('interface', [])
+ mac = config.get('mac', '')
+ kernel_interface = config.get('kernel_interface', '')
+
+ i = BondInterface(ifname, mode, lb, mac, kernel_interface)
+ i.add()
+ # Add members to bond
+ if members:
+ for member in members:
+ i.add_member(interface=member)
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py
new file mode 100644
index 000000000..c76d88480
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bridge.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos import ConfigError
+from vyos.vpp.interface import BridgeInterface
+from vyos.vpp.utils import iftunnel_transform
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Bridge interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Bridge interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'bridge']
+ vpp_interfaces = ['vpp', 'settings', 'interface']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global vpp interfaces for verify
+ config['vpp_interfaces'] = conf.get_config_dict(
+ vpp_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # determine which members have been removed
+ interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
+ if interfaces_removed:
+ config['members_removed'] = interfaces_removed
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config:
+ return None
+
+ # Check if interface exists in vpp before adding to bridge-domain
+
+ allowed_prefixes = ('gre', 'geneve', 'vxlan')
+
+ if 'member' in config:
+ for member in config.get('member', {}).get('interface', []):
+ # Check if the interface is explicitly listed or starts with allowed prefixes
+ if not (
+ member in config.get('vpp_interfaces', [])
+ or member.startswith(allowed_prefixes)
+ ):
+ raise ConfigError(
+ f"Interface '{member}' not found in 'vpp settings interface' or does not start with allowed prefixes {allowed_prefixes}"
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # vxlan10 in the vpp is vxlan_tunnel10
+ interface_transform_filter = ('geneve', 'vxlan')
+ # update members
+ if 'members_removed' in config:
+ i = BridgeInterface(ifname)
+ for member in config.get('members_removed'):
+ if member.startswith(interface_transform_filter):
+ member = iftunnel_transform(member)
+ i.detach_member(member=member)
+
+ # Delete bridge domain
+ if 'effective' in config:
+ ifname = config.get('ifname')
+ i = BridgeInterface(ifname)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add bridge domain
+ members = config.get('member', {}).get('interface', '')
+ i = BridgeInterface(ifname)
+ i.add()
+ # Add members to bridge
+ if members:
+ br = BridgeInterface(ifname)
+ for member in members:
+ if member.startswith(interface_transform_filter):
+ member = iftunnel_transform(member)
+ br.add_member(member=member)
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py
new file mode 100644
index 000000000..d82acb891
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ethernet.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdep import set_dependents
+from vyos import ConfigError
+from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Ethernet interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Ethernet interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'ethernet']
+ base_interfaces_xconnect = ['vpp', 'interfaces', 'xconnect']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not conf.exists(base + [ifname]):
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ config.update({'remove': effective_config})
+ else:
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get global 'vpp interfaces xconnect'
+ config['vpp_interfaces_xconnect'] = conf.get_config_dict(
+ base_interfaces_xconnect,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # Dependency
+ for xcon_name, xcon_config in config.get('vpp_interfaces_xconnect').items():
+ for member_name in xcon_config.get('member', {}).get('interface', []):
+ if member_name == ifname:
+ set_dependents('vpp_interfaces_xconnect', conf, xcon_name)
+
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ verify_vpp_remove_xconnect_interface(config)
+ if 'remove' in config:
+ return None
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ # Delete interface
+ if 'remove' in config:
+ pass
+ # remove_config = config.get('remove')
+ # ifname = config.get('ifname')
+ # src_addr = remove_config.get('source_address')
+ # dst_addr = remove_config.get('remote')
+ # vni = int(remove_config.get('vni'))
+ # v = VXLANInterface(ifname, src_addr, dst_addr, vni)
+ # v.delete()
+ else:
+ pass
+ # ifname = config.get('ifname')
+ # kernel_interface = config.get('kernel_interface', '')
+ # v = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface)
+ # v.add()
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py
new file mode 100644
index 000000000..a40cc9c84
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_geneve.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import GeneveInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Geneve interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Geneve interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'geneve']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces geneve geneveX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ required_keys = {'source_address', 'remote', 'vni'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # Change 'vpp interfaces geneve geneveX kernel-interface vpp-tunX'
+ # => 'vpp interfaces geneve geneveX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ vni = int(remove_config.get('vni'))
+ i = GeneveInterface(ifname, src_addr, dst_addr, vni)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ vni = int(config.get('vni'))
+ kernel_interface = config.get('kernel_interface', '')
+ i = GeneveInterface(ifname, src_addr, dst_addr, vni, kernel_interface)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py
new file mode 100644
index 000000000..b27234d18
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_gre.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import GREInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get GRE interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: GRE interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'gre']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces gre greX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ # config removed
+ if 'remove' in config:
+ return None
+
+ # source-address and remote are mandatory options
+ required_keys = {'source_address', 'remote'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # Change 'vpp interfaces gre greX kernel-interface vpp-tunX'
+ # => 'vpp interfaces gre greX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ i = GREInterface(ifname, src_addr, dst_addr)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ kernel_interface = config.get('kernel_interface', '')
+ i = GREInterface(ifname, src_addr, dst_addr, kernel_interface)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py
new file mode 100644
index 000000000..e0f1258d0
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ipip.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+
+from vyos.vpp.interface import IPIPInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get IPIP interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: IPIP interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'ipip']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if config.get('effective', {}).get('kernel_interface'):
+ iface = config.get('kernel_interface')
+ if iface:
+ if iface in config.get('vpp_kernel_interfaces'):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces ipip ipipX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ # config removed
+ if 'remove' in config:
+ return None
+
+ # source-address and remote are mandatory options
+ required_keys = {'source_address', 'remote'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # Change 'vpp interfaces ipip ipipX kernel-interface vpp-tunX'
+ # => 'vpp interfaces ipip ipipX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ i = IPIPInterface(ifname, src_addr, dst_addr)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ kernel_interface = config.get('kernel_interface', '')
+ i = IPIPInterface(ifname, src_addr, dst_addr, kernel_interface)
+ i.add()
+
+ if 'kernel_interface' in config:
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py
new file mode 100644
index 000000000..67fd049f0
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_loopback.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import LoopbackInterface
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Loopback interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Loopback interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'loopback']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ verify_vpp_remove_kernel_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ i = LoopbackInterface(ifname)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ kernel_interface = config.get('kernel_interface', '')
+ i = LoopbackInterface(ifname, kernel_interface)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py
new file mode 100644
index 000000000..5c957c7fa
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_vxlan.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import VXLANInterface
+from vyos.vpp.config_deps import deps_bridge_dict
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get VXLAN interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: VXLAN interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'vxlan']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ config['bridge_members'] = deps_bridge_dict(conf)
+ if ifname in config['bridge_members']:
+ for bridge_iface in config['bridge_members'][ifname]:
+ set_dependents('vpp_interfaces_bridge', conf, bridge_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces vxlan vxlanX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ required_keys = {'source_address', 'remote', 'vni'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # Change 'vpp interfaces vxlan greX kernel-interface vpp-tunX'
+ # => 'vpp interfaces gre vxlanX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ vni = int(remove_config.get('vni'))
+ i = VXLANInterface(ifname, src_addr, dst_addr, vni)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ vni = int(config.get('vni'))
+ kernel_interface = config.get('kernel_interface', '')
+ i = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py
new file mode 100644
index 000000000..dd1348b7c
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_xconnect.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.interface import XconnectInterface
+from vyos.vpp.utils import cli_ifaces_list
+
+
+def get_config(config=None, recursive_defaults=True) -> dict:
+ """Get Xconnect interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Bridge interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'xconnect']
+ vpp_interfaces = ['vpp', 'settings', 'interface']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=recursive_defaults,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ # Get global vpp interfaces for verify
+ config['vpp_interfaces'] = conf.get_config_dict(
+ vpp_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate')
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config:
+ return None
+
+ # Xconnect requires 2 members
+ if len(config.get('member', {}).get('interface')) != 2:
+ raise ConfigError('Cross connect requires 2 members')
+
+ # Member must belong to VPP
+ for iface in config.get('member', {}).get('interface', []):
+ if iface not in config['vpp_ifaces']:
+ raise ConfigError(f'{iface} must be a VPP interface for xconnect')
+
+ # Each interface can belong only to one xconnect
+ for xconn_member, xconn_ifaces in config['xconn_members'].items():
+ if len(xconn_ifaces) > 1:
+ raise ConfigError(
+ f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+
+ # Delete xconnect
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ members = remove_config.get('member', {}).get('interface')
+ i = XconnectInterface(ifname, members=members)
+ i.del_l2_xconnect()
+
+ if 'remove' in config:
+ return None
+
+ # Add xconnect
+ members = config.get('member', {}).get('interface')
+ i = XconnectInterface(ifname, members=members)
+ i.add_l2_xconnect()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py
new file mode 100644
index 000000000..4e4ecb442
--- /dev/null
+++ b/src/conf_mode/vpp_kernel-interfaces.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import node_changed
+from vyos import ConfigError
+from vyos.ifconfig import Interface
+from vyos.utils.network import interface_exists
+from vyos.utils.process import call
+from vyos.vpp import VPPControl
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ )
+
+ # Get effective config as we need full dicitonary per interface delete
+ if __name__ == '__main__':
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ # if a file was started as dependency, we are starting from empty config
+ else:
+ effective_config = {}
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ address_removed = leaf_node_changed(conf, base + [ifname, 'address'])
+ if address_removed:
+ config['address_removed'] = address_removed
+
+ description_removed = leaf_node_changed(conf, base + [ifname, 'description'])
+ if description_removed:
+ config['description_removed'] = {}
+
+ vlans_removed = node_changed(conf, base + [ifname, 'vif'])
+ if vlans_removed:
+ config['vlans_removed'] = vlans_removed
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config:
+ return None
+
+ # Interface must exists before it is configured
+ if not interface_exists(config['ifname']):
+ raise ConfigError(
+ f'Interface {config["ifname"]} must be created before using in configuration'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ ifname = config.get('ifname')
+ i = Interface(ifname)
+ # update/remove addresses
+ if 'address_removed' in config:
+ for address in config['address_removed']:
+ i.del_addr(address)
+ # remove description
+ if 'description_removed' in config:
+ i.set_alias('')
+
+ # remove VLANs
+ if 'vlans_removed' in config:
+ for vlan in config['vlans_removed']:
+ call(f'ip link del dev {ifname}.{vlan}')
+
+ # Delete
+ if 'remove' in config:
+ pass
+ else:
+ # Add address
+ if 'address' in config:
+ for address in config['address']:
+ i.add_addr(address)
+ # Set MTU
+ if 'mtu' in config:
+ i.set_mtu(config.get('mtu'))
+ # Set description
+ if 'description' in config:
+ i.set_alias(config.get('description'))
+ # Admin state down
+ if 'disable' in config:
+ i.set_admin_state('down')
+ else:
+ i.set_admin_state('up')
+
+ for vlan, vlan_config in config.get('vif', {}).items():
+ if vlan not in config.get('effective', {}).get('vif', {}).keys():
+ call(
+ f'ip link add link {ifname} name {ifname}.{vlan} type vlan id {vlan}'
+ )
+ call(f'ip link set dev {ifname}.{vlan} up')
+ v = Interface(f'{ifname}.{vlan}')
+
+ # VLAN address
+ addresses_effective = (
+ config.get('effective', {})
+ .get('vif', {})
+ .get(vlan, {})
+ .get('address', [])
+ )
+ addresses_candidate = vlan_config.get('address', [])
+
+ for ipaddr in addresses_effective:
+ if ipaddr not in addresses_candidate:
+ v.del_addr(ipaddr)
+ for ipaddr in addresses_candidate:
+ if ipaddr not in addresses_effective:
+ v.add_addr(ipaddr)
+
+ # VLAN description
+ description_effective = (
+ config.get('effective', {})
+ .get('vif', {})
+ .get(vlan, {})
+ .get('description', '')
+ )
+ description_candidate = vlan_config.get('description', '')
+
+ if description_candidate:
+ v.set_alias(description_candidate)
+ elif description_effective and not description_candidate:
+ v.set_alias('')
+
+ v.set_admin_state('up')
+
+ # Set rx-mode
+ rx_mode = config.get('rx_mode')
+ if rx_mode:
+ vpp_control = VPPControl()
+ lcp_name = vpp_control.lcp_pair_find(kernel_name=ifname).get('vpp_name_kernel')
+ vpp_control.iface_rxmode(lcp_name, rx_mode)
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py
new file mode 100755
index 000000000..b7c42eb58
--- /dev/null
+++ b/src/op_mode/show_vpp_interfaces.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import argparse
+import json
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import rc_cmd
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import (
+ vpp_ifaces_list,
+ vpp_ip_addresses_by_index,
+ vpp_ifaces_stats,
+)
+
+
+def get_iproute_address_list(interface: str) -> list:
+ """Get data from the Linux command 'ip --json address list dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json address list' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json address list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def get_iproute_link_list(interface):
+ """Get data from the Linux command 'ip --json link show dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json link show' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json link list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def merge_dicts(*dicts) -> dict:
+ """Merge dictionaries into a new dictionary.
+
+ Args:
+ *dicts: Any number of dictionaries.
+
+ Returns:
+ dict: A new dictionary containing all the key-value pairs from the given dictionaries.
+ """
+ merged = {}
+ for dictionary in dicts:
+ merged.update(dictionary)
+ return merged
+
+
+def show_interfaces(interfaces_list: list) -> str:
+ """Get JSON info from linux and represent it in a table format
+ Use tabulate to generate table
+
+ Interface IP Address Mtu S/L Description
+ --------- ---------- --- --- -----------
+ dum0 203.0.113.1/32 1500 u/u
+ 100.64.1.1/24
+ eth0 192.168.122.14/24 1500 u/u WAN
+
+ :return:
+ """
+ table = []
+ for interface in interfaces_list:
+ # Get the data for the interface
+ ip_address_data = get_iproute_address_list(interface)
+ link_data = get_iproute_link_list(interface)
+
+ # Skip this interface if data is not available
+ if not link_data:
+ continue
+ interface_data = merge_dicts(ip_address_data[0], link_data[0])
+
+ # Get the interface name
+ interface_name = interface_data['ifname']
+
+ # Get the IP addresses and their corresponding prefixes
+ ip_info = [
+ (address['local'], address.get('prefixlen', ''))
+ for address in interface_data['addr_info']
+ ]
+
+ # Format the IP addresses with prefixes and line breaks
+ ip_addresses = '\n'.join(f'{ip}/{prefix}' for ip, prefix in ip_info)
+
+ # Get the MAC address
+ mac = interface_data.get('address', 'n/a')
+
+ # Get the MTU
+ mtu = interface_data.get('mtu')
+
+ # Get the state of the interface
+ state = interface_data['operstate'].lower()
+
+ # Get the description of the interface
+ description = interface_data.get('ifalias', '')
+
+ # Create the list of values for the table
+ values = [interface_name, ip_addresses, mac, mtu, state, description]
+
+ # Append the list of values to the table
+ table.append(values)
+
+ # Print the table with IP addresses listed on separate lines
+ headers = ['Interface', 'IP Address', 'MAC', 'MTU', 'State', 'Description']
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_dataplane(interfaces_list: list, filter_type: str = 'all') -> str:
+ table = []
+ interface_dp_filter = ('tun', 'tap')
+ lcp_pair_list = vpp.lcp_pairs_list()
+ vpp_name_kernel_to_kernel_name = {
+ entry['vpp_name_kernel']: entry['kernel_name'] for entry in lcp_pair_list
+ }
+ for interface in interfaces_list:
+ interface_name = interface.get('interface_name')
+ if filter_type == 'no_tun_tap' and interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ if filter_type == 'only_tun_tap' and not interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ kernel_name = vpp_name_kernel_to_kernel_name.get(interface_name, '')
+
+ dp_ip_addresses = vpp_ip_addresses_by_index(
+ vpp.api, interface.get('sw_if_index')
+ )
+ ip_addresses = '\n'.join(dp_ip_addresses)
+
+ mac = str(interface.get('l2_address', 'n/a'))
+ mtu = interface.get('mtu', [])[0]
+ # state
+ flags = interface.get('flags')
+ state = 'up' if flags == 3 else 'down'
+
+ iftype = interface.get('interface_dev_type').split()[0]
+
+ values = [kernel_name, interface_name, iftype, ip_addresses, mac, mtu, state]
+ table.append(values)
+ headers = [
+ 'Kernel',
+ 'Dataplane',
+ 'Type',
+ 'IP Address',
+ 'MAC',
+ 'MTU',
+ 'State',
+ ]
+ table = sorted(table)
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_hardware(intf_name) -> str:
+ if not intf_name:
+ intf_name = ''
+
+ statistics = vpp_ifaces_stats(intf_name)
+ for intf, stats in sorted(statistics.items()):
+ print(f'\n---------------------------------\nInterface {intf}:\n')
+ table = []
+ for k, v in stats.items():
+ if isinstance(v, dict):
+ for i, j in v.items():
+ table.append([f"{k} {i}", j])
+ else:
+ table.append([k, v])
+ print(tabulate(table, tablefmt="presto"))
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Show VPP interfaces')
+ parser.add_argument(
+ '--summary',
+ action='store_true',
+ help='Show summary of VPP interfaces (ethernet and kernel tun)',
+ )
+ parser.add_argument(
+ '--dataplane', action='store_true', help='Show VPP ethernet interfaces'
+ )
+ parser.add_argument(
+ '--kernel', action='store_true', help='Show VPP kernel interfaces'
+ )
+ parser.add_argument(
+ '--iproute', action='store_true', help='Show interfaces (iproute2)'
+ )
+ parser.add_argument(
+ '--hardware',
+ action='store_true',
+ help='Show more detailed statistics for VPP interfaces',
+ )
+ parser.add_argument('--intf-name', action='store', help='Kernel interface name')
+
+ args = parser.parse_args()
+
+ config = ConfigTreeQuery()
+
+ if not config.exists('vpp settings interface'):
+ print('VPP interfaces not configured')
+ exit(0)
+
+ vpp = VPPControl()
+ dp_ifaces_list = vpp_ifaces_list(vpp.api)
+
+ if args.summary:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='all'))
+
+ if args.dataplane:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='no_tun_tap'))
+ exit(0)
+
+ if args.kernel:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='only_tun_tap'))
+
+ if args.iproute:
+ vpp_interfaces = []
+ vpp_ethernet = config.list_nodes('vpp settings interface')
+ vpp_interfaces.extend(vpp_ethernet)
+ if config.exists('vpp kernel-interfaces'):
+ vpp_kernel_interfaces = config.list_nodes('vpp kernel-interfaces')
+ vpp_interfaces.extend(vpp_kernel_interfaces)
+ print(show_interfaces(interfaces_list=vpp_interfaces))
+
+ if args.hardware:
+ show_interfaces_hardware(intf_name=args.intf_name)