summaryrefslogtreecommitdiff
path: root/src
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 /src
parentdef342ffc4244e345f02685c12dc157372d5dd68 (diff)
downloadvyos-1x-bb4427bfb4a8c03784dd6f097cbe521f561edaf4.tar.gz
vyos-1x-bb4427bfb4a8c03784dd6f097cbe521f561edaf4.zip
Add vyos-vpp CLI and python3 modules
Diffstat (limited to 'src')
-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
13 files changed, 2577 insertions, 0 deletions
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)