From bb4427bfb4a8c03784dd6f097cbe521f561edaf4 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Tue, 7 Jan 2025 21:54:47 +0000 Subject: Add vyos-vpp CLI and python3 modules --- python/vyos/vpp/__init__.py | 20 ++ python/vyos/vpp/config_deps.py | 76 ++++++ python/vyos/vpp/config_filter.py | 58 +++++ python/vyos/vpp/config_verify.py | 160 ++++++++++++ python/vyos/vpp/configdb.py | 206 +++++++++++++++ python/vyos/vpp/control_host.py | 363 +++++++++++++++++++++++++++ python/vyos/vpp/control_vpp.py | 444 +++++++++++++++++++++++++++++++++ python/vyos/vpp/interface/__init__.py | 40 +++ python/vyos/vpp/interface/bond.py | 113 +++++++++ python/vyos/vpp/interface/bridge.py | 128 ++++++++++ python/vyos/vpp/interface/ethernet.py | 54 ++++ python/vyos/vpp/interface/geneve.py | 90 +++++++ python/vyos/vpp/interface/gre.py | 85 +++++++ python/vyos/vpp/interface/ipip.py | 83 ++++++ python/vyos/vpp/interface/loopback.py | 68 +++++ python/vyos/vpp/interface/vxlan.py | 94 +++++++ python/vyos/vpp/interface/wireguard.py | 56 +++++ python/vyos/vpp/interface/xconnect.py | 94 +++++++ python/vyos/vpp/nat/__init__.py | 0 python/vyos/vpp/utils.py | 281 +++++++++++++++++++++ 20 files changed, 2513 insertions(+) create mode 100644 python/vyos/vpp/__init__.py create mode 100644 python/vyos/vpp/config_deps.py create mode 100644 python/vyos/vpp/config_filter.py create mode 100644 python/vyos/vpp/config_verify.py create mode 100644 python/vyos/vpp/configdb.py create mode 100644 python/vyos/vpp/control_host.py create mode 100644 python/vyos/vpp/control_vpp.py create mode 100644 python/vyos/vpp/interface/__init__.py create mode 100644 python/vyos/vpp/interface/bond.py create mode 100644 python/vyos/vpp/interface/bridge.py create mode 100644 python/vyos/vpp/interface/ethernet.py create mode 100644 python/vyos/vpp/interface/geneve.py create mode 100644 python/vyos/vpp/interface/gre.py create mode 100644 python/vyos/vpp/interface/ipip.py create mode 100644 python/vyos/vpp/interface/loopback.py create mode 100644 python/vyos/vpp/interface/vxlan.py create mode 100644 python/vyos/vpp/interface/wireguard.py create mode 100644 python/vyos/vpp/interface/xconnect.py create mode 100644 python/vyos/vpp/nat/__init__.py create mode 100644 python/vyos/vpp/utils.py (limited to 'python') 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 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 kernel-interface vpp-tunX' + commit + set vpp interfaces gre|vxlan 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\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?Peth\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\w+:\w+) subsystem (?P\w+:\w+) address (?P
\w+:\w+:\w+\.\w+) numa (?P\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 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[^/]+)/(?P[^/]+)') + # 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 -- cgit v1.2.3 From 10de7ce46af1d38edfc449800ce730ad9bb4fbaa Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Sat, 11 Jan 2025 14:20:29 +0000 Subject: Move VPPContron to interface init --- python/vyos/vpp/interface/bond.py | 33 +++++++++++++++++---------------- python/vyos/vpp/interface/bridge.py | 17 ++++++++--------- python/vyos/vpp/interface/ethernet.py | 9 ++++----- python/vyos/vpp/interface/geneve.py | 13 ++++++------- python/vyos/vpp/interface/gre.py | 13 ++++++------- python/vyos/vpp/interface/ipip.py | 15 +++++++-------- python/vyos/vpp/interface/loopback.py | 17 +++++++++-------- python/vyos/vpp/interface/vxlan.py | 13 ++++++------- python/vyos/vpp/interface/wireguard.py | 13 ++++++------- python/vyos/vpp/interface/xconnect.py | 21 ++++++++++----------- 10 files changed, 79 insertions(+), 85 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py index 1cc306670..28aae74a1 100644 --- a/python/vyos/vpp/interface/bond.py +++ b/python/vyos/vpp/interface/bond.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl from vyos.vpp.control_host import set_promisc -vpp = VPPControl() - class BondInterface: def __init__( @@ -36,6 +34,7 @@ class BondInterface: self.load_balance = load_balance self.mac = mac self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create Bond interface @@ -53,9 +52,9 @@ class BondInterface: } if self.mac: create_args.update({'use_custom_mac': True, 'mac_address': self.mac}) - vpp.api.bond_create2(**create_args) + self.vpp.api.bond_create2(**create_args) if self.kernel_interface: - vpp.lcp_pair_add(self.ifname, self.kernel_interface) + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) def delete(self): """Delete Bond interface @@ -64,8 +63,8 @@ class 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) + bond_if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.bond_delete(sw_if_index=bond_if_index) def add_member(self, interface): """Add member to Bond interface @@ -74,13 +73,15 @@ class 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_if_index = self.vpp.get_sw_if_index(f'BondEthernet{self.instance}') + member_if_index = self.vpp.get_sw_if_index(interface) + member_if_type = self.vpp.get_sw_if_dev_type(interface) + self.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) + self.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') @@ -91,8 +92,8 @@ class 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) + member_if_index = self.vpp.get_sw_if_index(interface) + self.vpp.api.bond_detach_member(sw_if_index=member_if_index) def kernel_add(self): """Add LCP pair @@ -101,7 +102,7 @@ class BondInterface: a = BondInterface(ifname='bond0', mode=5) a.kernel_add() """ - vpp.lcp_pair_add(self.ifname, self.kernel_interface) + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) def kernel_delete(self): """Delete LCP pair @@ -110,4 +111,4 @@ class BondInterface: a = BondInterface(ifname='bond0', mode=5) a.kernel_delete() """ - vpp.lcp_pair_del(self.ifname, self.kernel_interface) + self.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 index e220547de..407eab08c 100644 --- a/python/vyos/vpp/interface/bridge.py +++ b/python/vyos/vpp/interface/bridge.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023-2024 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -17,8 +17,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - class BridgeInterface: def __init__( @@ -37,6 +35,7 @@ class BridgeInterface: self.learn = learn self.uu_flood = uu_flood self.arp_term = arp_term + self.vpp = VPPControl() def add(self): """Create Bridge interface @@ -49,7 +48,7 @@ class BridgeInterface: a = BridgeInterface(ifname='br23') a.add() """ - vpp.api.bridge_domain_add_del_v2( + self.vpp.api.bridge_domain_add_del_v2( is_add=True, bd_id=self.interface_suffix, flood=self.flood, @@ -69,7 +68,7 @@ class BridgeInterface: a = BridgeInterface(ifname='br23') a.delete() """ - vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix) + self.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 @@ -94,9 +93,9 @@ class BridgeInterface: elif member.isdigit(): member_if_index = int(member) else: - member_if_index = vpp.get_sw_if_index(member) + member_if_index = self.vpp.get_sw_if_index(member) - return vpp.api.sw_interface_set_l2_bridge( + return self.vpp.api.sw_interface_set_l2_bridge( rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=0 ) @@ -121,8 +120,8 @@ class BridgeInterface: elif member.isdigit(): member_if_index = int(member) else: - member_if_index = vpp.get_sw_if_index(member) + member_if_index = self.vpp.get_sw_if_index(member) - return vpp.api.sw_interface_set_l2_bridge( + return self.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 index ddd3dc42f..368d94c7f 100644 --- a/python/vyos/vpp/interface/ethernet.py +++ b/python/vyos/vpp/interface/ethernet.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Ethernet interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - class EthernetInterface: """Interface Ethernet""" @@ -28,6 +26,7 @@ class EthernetInterface: self.instance = int(ifname.removeprefix('eth')) self.ifname = ifname self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): pass @@ -42,7 +41,7 @@ class EthernetInterface: a = EthernetInterface(ifname='eth0') a.kernel_add() """ - vpp.lcp_pair_add(self.ifname, self.kernel_interface) + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) def kernel_delete(self): """Delete LCP pair @@ -51,4 +50,4 @@ class EthernetInterface: a = EthernetInterface(ifname='eth0') a.kernel_delete() """ - vpp.lcp_pair_del(self.ifname, self.kernel_interface) + self.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 index 3d5e75003..a55e29d80 100644 --- a/python/vyos/vpp/interface/geneve.py +++ b/python/vyos/vpp/interface/geneve.py @@ -1,6 +1,6 @@ # VyOS implementation of Geneve interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - def show(): """Show Geneve interface @@ -38,6 +36,7 @@ class GeneveInterface: self.dst_address = remote self.vni = vni self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create Geneve interface @@ -48,7 +47,7 @@ class 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( + return self.vpp.api.geneve_add_del_tunnel2( is_add=True, local_address=self.src_address, remote_address=self.dst_address, @@ -63,7 +62,7 @@ class 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( + return self.vpp.api.geneve_add_del_tunnel2( is_add=False, local_address=self.src_address, remote_address=self.dst_address, @@ -78,7 +77,7 @@ class 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') + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') def kernel_delete(self): """Delete LCP pair @@ -87,4 +86,4 @@ class 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) + self.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 index 0017127ec..8a9128836 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP GRE interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - def show(): """Show GRE interface @@ -37,6 +35,7 @@ class GREInterface: self.src_address = source_address self.dst_address = remote self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create GRE interface @@ -46,7 +45,7 @@ class GREInterface: a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25') a.add() """ - vpp.api.gre_tunnel_add_del( + self.vpp.api.gre_tunnel_add_del( is_add=True, tunnel={ 'src': self.src_address, @@ -62,7 +61,7 @@ class 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( + return self.vpp.api.gre_tunnel_add_del( is_add=False, tunnel={'src': self.src_address, 'dst': self.dst_address} ) @@ -73,7 +72,7 @@ class 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') + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') def kernel_delete(self): """Delete LCP pair @@ -82,4 +81,4 @@ class 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) + self.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 index e1f19735b..031b0a698 100644 --- a/python/vyos/vpp/interface/ipip.py +++ b/python/vyos/vpp/interface/ipip.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP IPIP interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - def show(): """Show IPIP interface @@ -37,6 +35,7 @@ class IPIPInterface: self.src_address = source_address self.dst_address = remote self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create IPIP interface @@ -46,7 +45,7 @@ class IPIPInterface: a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5') a.add() """ - vpp.api.ipip_add_tunnel( + self.vpp.api.ipip_add_tunnel( tunnel={ 'src': self.src_address, 'dst': self.dst_address, @@ -61,8 +60,8 @@ class 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) + ipip_if_index = self.vpp.get_sw_if_index(f'ipip{self.instance}') + return self.vpp.api.ipip_del_tunnel(sw_if_index=ipip_if_index) def kernel_add(self): """Add LCP pair @@ -71,7 +70,7 @@ class 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') + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') def kernel_delete(self): """Delete LCP pair @@ -80,4 +79,4 @@ class 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) + self.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 index df7b82356..8a5414d54 100644 --- a/python/vyos/vpp/interface/loopback.py +++ b/python/vyos/vpp/interface/loopback.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Loopback interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - class LoopbackInterface: """Interface Loopback""" @@ -28,6 +26,7 @@ class LoopbackInterface: self.instance = int(ifname.removeprefix('lo')) self.ifname = f'loop{self.instance}' self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create Loopback interface @@ -37,7 +36,9 @@ class LoopbackInterface: a = LoopbackInterface(ifname='lo1') a.add() """ - vpp.api.create_loopback_instance(is_specified=True, user_instance=self.instance) + self.vpp.api.create_loopback_instance( + is_specified=True, user_instance=self.instance + ) def delete(self): """Delete Loopback interface @@ -46,8 +47,8 @@ class 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) + loopback_if_index = self.vpp.get_sw_if_index(f'loop{self.instance}') + return self.vpp.api.delete_loopback(sw_if_index=loopback_if_index) def kernel_add(self): """Add LCP pair @@ -56,7 +57,7 @@ class LoopbackInterface: a = LoopbackInterface(ifname='lo1') a.kernel_add() """ - vpp.lcp_pair_add(self.ifname, self.kernel_interface) + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) def kernel_delete(self): """Delete LCP pair @@ -65,4 +66,4 @@ class LoopbackInterface: a = LoopbackInterface(ifname='lo1') a.kernel_delete() """ - vpp.lcp_pair_del(self.ifname, self.kernel_interface) + self.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 index 907fcbee0..91e7b3059 100644 --- a/python/vyos/vpp/interface/vxlan.py +++ b/python/vyos/vpp/interface/vxlan.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP VXLAN interface # -# Copyright (C) 2023-2024 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - def show(): """Show VXLAN interface @@ -40,6 +38,7 @@ class VXLANInterface: self.dst_address = remote self.vni = vni self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create VXLAN interface @@ -50,7 +49,7 @@ class 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( + self.vpp.api.vxlan_add_del_tunnel_v3( is_add=True, src_address=self.src_address, dst_address=self.dst_address, @@ -67,7 +66,7 @@ class 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( + return self.vpp.api.vxlan_add_del_tunnel_v3( is_add=False, src_address=self.src_address, dst_address=self.dst_address, @@ -82,7 +81,7 @@ class 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) + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) def kernel_delete(self): """Delete LCP pair @@ -91,4 +90,4 @@ class 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) + self.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 index 582929851..649ed5b05 100644 --- a/python/vyos/vpp/interface/wireguard.py +++ b/python/vyos/vpp/interface/wireguard.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Wireguard interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -18,8 +18,6 @@ from vyos.vpp import VPPControl -vpp = VPPControl() - class WireguardInterface: """Interface Wireguard""" @@ -29,6 +27,7 @@ class WireguardInterface: self.ifname = ifname self.listen_port = listen_port self.kernel_interface = kernel_interface + self.vpp = VPPControl() def add(self): """Create Wireguard interface @@ -38,12 +37,12 @@ class WireguardInterface: a = WireguardInterface(ifname='wg5', listen_port=51820, generate_key=True) a.add() """ - vpp.api.wireguard_interface_create( + self.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') + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') def delete(self): """Delete Wireguard interface @@ -52,5 +51,5 @@ class 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) + wg_if_index = self.vpp.get_sw_if_index(f'wg{self.instance}') + return self.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 index b4bec3321..41cf773d9 100644 --- a/python/vyos/vpp/interface/xconnect.py +++ b/python/vyos/vpp/interface/xconnect.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP bridge interface # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -19,8 +19,6 @@ from vyos.vpp import VPPControl from vyos.vpp.utils import iftunnel_transform -vpp = VPPControl() - class XconnectInterface: def __init__( @@ -30,6 +28,7 @@ class XconnectInterface: ): self.ifname = ifname self.members = members + self.vpp = VPPControl() def add_l2_xconnect(self): """Add l2 cross connect @@ -49,14 +48,14 @@ class XconnectInterface: 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( + member_first_if_index = self.vpp.get_sw_if_index(first_member) + member_second_if_index = self.vpp.get_sw_if_index(second_member) + self.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( + self.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, @@ -80,14 +79,14 @@ class XconnectInterface: 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( + member_first_if_index = self.vpp.get_sw_if_index(first_member) + member_second_if_index = self.vpp.get_sw_if_index(second_member) + self.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( + self.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, -- cgit v1.2.3 From bd2ea9f20502171b12f8bb4d900f504af9c446f1 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Sat, 11 Jan 2025 15:12:42 +0000 Subject: Use VPPControl for the show class interfaces --- python/vyos/vpp/interface/geneve.py | 1 + python/vyos/vpp/interface/gre.py | 1 + python/vyos/vpp/interface/ipip.py | 1 + python/vyos/vpp/interface/vxlan.py | 1 + 4 files changed, 4 insertions(+) (limited to 'python') diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py index a55e29d80..8260b1bfa 100644 --- a/python/vyos/vpp/interface/geneve.py +++ b/python/vyos/vpp/interface/geneve.py @@ -25,6 +25,7 @@ def show(): from vyos.vpp.interface import geneve geneve.show() """ + vpp = VPPControl() return vpp.api.geneve_tunnel_dump() diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py index 8a9128836..20a7ae9d4 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -25,6 +25,7 @@ def show(): from vyos.vpp.interface import gre gre.show() """ + vpp = VPPControl() return vpp.api.gre_tunnel_dump() diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py index 031b0a698..1aa1e22f2 100644 --- a/python/vyos/vpp/interface/ipip.py +++ b/python/vyos/vpp/interface/ipip.py @@ -25,6 +25,7 @@ def show(): from vyos.vpp.interface import ipip ipip.show() """ + vpp = VPPControl() return vpp.api.ipip_tunnel_dump() diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py index 91e7b3059..35b3f9fc1 100644 --- a/python/vyos/vpp/interface/vxlan.py +++ b/python/vyos/vpp/interface/vxlan.py @@ -25,6 +25,7 @@ def show(): from vyos.vpp.interface import vxlan vxlan.show() """ + vpp = VPPControl() return vpp.api.vxlan_tunnel_dump() -- cgit v1.2.3 From 1b6e491537c98d396b25aaf734b66b6ba5413dac Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Tue, 14 Jan 2025 12:29:23 +0200 Subject: vpp: add example of adding vxlan kernel_add --- python/vyos/vpp/interface/vxlan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py index 35b3f9fc1..36206e8c4 100644 --- a/python/vyos/vpp/interface/vxlan.py +++ b/python/vyos/vpp/interface/vxlan.py @@ -79,7 +79,7 @@ class VXLANInterface: """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 = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23, kernel_interface='vpptap10') a.kernel_add() """ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) -- cgit v1.2.3 From 63bd816bdfcc041d10941dab51a9a0792218ef42 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Tue, 21 Jan 2025 17:23:50 +0000 Subject: bonding: set interface state up after adding By default VPP creates interface via API with the 'down' state. Add methods to set interface state UP/DOWN. It probably should reuse the common Class in the future. We do not have classes for interface (vpp) state settings. --- python/vyos/vpp/interface/bond.py | 20 ++++++++++++++++++++ src/conf_mode/vpp_interfaces_bonding.py | 2 ++ 2 files changed, 22 insertions(+) (limited to 'python') diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py index 28aae74a1..a90f2950b 100644 --- a/python/vyos/vpp/interface/bond.py +++ b/python/vyos/vpp/interface/bond.py @@ -112,3 +112,23 @@ class BondInterface: a.kernel_delete() """ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) + + def set_state_up(self): + """Set Bond interface state to UP + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0') + a.set_state_up() + """ + bond_if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.sw_interface_set_flags(sw_if_index=bond_if_index, flags=1) + + def set_state_down(self): + """Set Bond interface state to DOWN + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0') + a.set_state_down() + """ + bond_if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.sw_interface_set_flags(sw_if_index=bond_if_index, flags=0) diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py index e1919e946..0ec4f7151 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -202,6 +202,8 @@ def apply(config): for member in members: i.add_member(interface=member) + i.set_state_up() + call_dependents() return None -- cgit v1.2.3 From 20ecb6d4ccaf903bcaaf658b1a655dd8d79b989c Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Wed, 22 Jan 2025 15:01:05 +0000 Subject: GRE: Add tunnel-type erspan, l3 and teb Add tunnel type - erspan - l3 - teb (Transparent Ethernet Bridge) By default L3 GRE interfaces cannot be bridged to a bridge interface. Add the ability to change tunnel type. set vpp interfaces gre gre2 tunnel-type 'teb' --- interface-definitions/vpp.xml.in | 25 +++++++++++++++++++++ python/vyos/vpp/interface/gre.py | 45 ++++++++++++++++++++++++++++++++++--- smoketest/scripts/cli/test_vpp.py | 4 ++-- src/conf_mode/vpp_interfaces_gre.py | 5 +++-- 4 files changed, 72 insertions(+), 7 deletions(-) (limited to 'python') diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 0718ba0fe..c946839db 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -180,6 +180,31 @@ + + + GRE tunnel type + + erspan l3 teb + + + erspan + Encapsulated Remote Switched Port Analyzer + + + l3 + Generic Routing Encapsulation (network layer) + + + teb + L2 Transparent Ethernet Bridge + + + (erspan|l3|teb) + + Invalid encapsulation, must be one of: l3, teb or erspan + + l3 + #include #include #include diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py index 20a7ae9d4..65559f294 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -30,20 +30,58 @@ def show(): class GREInterface: - def __init__(self, ifname, source_address, remote, kernel_interface: str = ''): + """ + Class representing a GRE (Generic Routing Encapsulation) interface. + + Attributes: + ifname (str): The interface name. + source_address (str): The source IP address for the GRE tunnel. + remote (str): The remote IP address for the GRE tunnel. + tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. + kernel_interface (str): The associated kernel interface. Defaults to an empty string. + instance (int): The instance number derived from the interface name. + vpp (VPPControl): An instance of the VPPControl class for interacting with the VPP API. + """ + + # Mapping of tunnel types https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api#L25-L35 + TUNNEL_TYPE_MAP = { + "l3": 0, + "teb": 1, + "erspan": 2, + } + + def __init__( + self, + ifname, + source_address, + remote, + tunnel_type: str = 'l3', + kernel_interface: str = '', + ): + """ + Initialize a GREInterface instance. + + Args: + ifname (str): The interface name. + source_address (str): The source IP address for the GRE tunnel. + remote (str): The remote IP address for the GRE tunnel. + tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. + kernel_interface (str): The associated kernel interface. Defaults to an empty string. + """ self.instance = int(ifname.removeprefix('gre')) self.ifname = ifname self.src_address = source_address self.dst_address = remote + self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type] self.kernel_interface = kernel_interface self.vpp = VPPControl() def add(self): """Create GRE interface - https://github.com/FDio/vpp/blob/stable/2306/src/plugins/gre/gre.api + https://github.com/FDio/vpp/blob/stable/2406/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 = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25', tunnel_type='l3') a.add() """ self.vpp.api.gre_tunnel_add_del( @@ -52,6 +90,7 @@ class GREInterface: 'src': self.src_address, 'dst': self.dst_address, 'instance': self.instance, + 'type': self.tunnel_type, }, ) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 582c41c68..ba0d0d728 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -552,7 +552,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertRegex( normalized_out, r'BondEthernet23\s+\d+\s+up', - "Interface BondEthernet23 is not in the expected state 'up'." + "Interface BondEthernet23 is not in the expected state 'up'.", ) # set kernel interface @@ -617,7 +617,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertRegex( normalized_out, r'BondEthernet23\s+\d+\s+up', - "Interface BondEthernet23 is not in the expected state 'up'." + "Interface BondEthernet23 is not in the expected state 'up'.", ) # delete vpp kernel-interface vlan diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index feff0d6b5..4cb1a71ea 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -124,7 +124,7 @@ def verify(config): return None # source-address and remote are mandatory options - required_keys = {'source_address', 'remote'} + required_keys = {'source_address', 'remote', 'tunnel_type'} if not all(key in config for key in required_keys): missing_keys = required_keys - set(config.keys()) raise ConfigError( @@ -159,7 +159,8 @@ def apply(config): 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) + tunnel_type = config.get('tunnel_type') + i = GREInterface(ifname, src_addr, dst_addr, tunnel_type, kernel_interface) i.add() # Add kernel-interface (LCP) if interface is not exist -- cgit v1.2.3 From 9425dce22f1bc8393d5ea6b2ae39b4ef70b7fa50 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Fri, 24 Jan 2025 15:26:27 +0000 Subject: GRE: Add ability to configure multipoint mode Add ability to configure multipoint mode. Remote IP address in this case has to be 0.0.0.0 Only one tunnel with the same source IP is allowed in the point-to-multipoint mode set vpp interfaces gre gre0 mode 'point-to-multipoint' set vpp interfaces gre gre0 remote '0.0.0.0' set vpp interfaces gre gre0 source-address '192.0.2.1' --- interface-definitions/vpp.xml.in | 21 +++++++++++++++++++++ python/vyos/vpp/interface/gre.py | 16 +++++++++++++--- src/conf_mode/vpp_interfaces_gre.py | 19 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) (limited to 'python') diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index c946839db..e3a2f9aff 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -180,6 +180,27 @@ + + + GRE tunnel mode + + point-to-point point-to-multipoint + + + point-to-point + Point to point mode + + + point-to-multipoint + Point to multipoint mode + + + (point-to-point|point-to-multipoint) + + Invalid mode, must be one of: point-to-point or point-to-multipoint + + point-to-point + GRE tunnel type diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py index 65559f294..8f3dbebae 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -38,6 +38,7 @@ class GREInterface: source_address (str): The source IP address for the GRE tunnel. remote (str): The remote IP address for the GRE tunnel. tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. + mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'. kernel_interface (str): The associated kernel interface. Defaults to an empty string. instance (int): The instance number derived from the interface name. vpp (VPPControl): An instance of the VPPControl class for interacting with the VPP API. @@ -45,9 +46,14 @@ class GREInterface: # Mapping of tunnel types https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api#L25-L35 TUNNEL_TYPE_MAP = { - "l3": 0, - "teb": 1, - "erspan": 2, + 'l3': 0, + 'teb': 1, + 'erspan': 2, + } + + MODE_MAP = { + 'point-to-point': 0, + 'point-to-multipoint': 1, } def __init__( @@ -56,6 +62,7 @@ class GREInterface: source_address, remote, tunnel_type: str = 'l3', + mode: str = 'point-to-point', kernel_interface: str = '', ): """ @@ -65,6 +72,7 @@ class GREInterface: ifname (str): The interface name. source_address (str): The source IP address for the GRE tunnel. remote (str): The remote IP address for the GRE tunnel. + mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'. tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. kernel_interface (str): The associated kernel interface. Defaults to an empty string. """ @@ -73,6 +81,7 @@ class GREInterface: self.src_address = source_address self.dst_address = remote self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type] + self.mode = self.MODE_MAP[mode] self.kernel_interface = kernel_interface self.vpp = VPPControl() @@ -90,6 +99,7 @@ class GREInterface: 'src': self.src_address, 'dst': self.dst_address, 'instance': self.instance, + 'mode': self.mode, 'type': self.tunnel_type, }, ) diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index 4cb1a71ea..334725994 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -124,13 +124,27 @@ def verify(config): return None # source-address and remote are mandatory options - required_keys = {'source_address', 'remote', 'tunnel_type'} + required_keys = {'source_address', 'remote', 'mode', 'tunnel_type'} 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('_', '-')}" ) + # check multipoint mode + if config.get('mode') == 'point-to-multipoint': + # For multipoint mode, remote IP must be 0.0.0.0 + if config.get('remote') != '0.0.0.0': + raise ConfigError('For point-to-multipoint mode, remote must be 0.0.0.0') + # Only one multipoint GRE tunnel is allowed from the same source address + for ifname, iface in config.get('vpp_interfaces', {}).items(): + if iface.get('mode') == 'point-to-multipoint' and iface.get( + 'source_address' + ) == config.get('source_address'): + raise ConfigError( + 'Only one multipoint GRE tunnel is allowed from the same source address' + ) + # 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' @@ -159,8 +173,9 @@ def apply(config): src_addr = config.get('source_address') dst_addr = config.get('remote') kernel_interface = config.get('kernel_interface', '') + mode = config.get('mode') tunnel_type = config.get('tunnel_type') - i = GREInterface(ifname, src_addr, dst_addr, tunnel_type, kernel_interface) + i = GREInterface(ifname, src_addr, dst_addr, tunnel_type, mode, kernel_interface) i.add() # Add kernel-interface (LCP) if interface is not exist -- cgit v1.2.3 From f76e349c11ae30d104ae09fef4e73c85f35d1885 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 27 Jan 2025 12:29:12 +0000 Subject: Bonding: Use common class for the interface state --- python/vyos/vpp/interface/interface.py | 0 src/conf_mode/vpp_interfaces_bonding.py | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 python/vyos/vpp/interface/interface.py (limited to 'python') diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py index 0ec4f7151..a93239d57 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -190,8 +190,9 @@ def apply(config): members = config.get('member', {}).get('interface', []) mac = config.get('mac', '') kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' - i = BondInterface(ifname, mode, lb, mac, kernel_interface) + i = BondInterface(ifname, mode, lb, mac, kernel_interface, state) # Introduce a delay to address instability in the VPP API, which may fail to create the LCP # or establish a connection. This should be reviewed and resolved in future releases. time.sleep(2) @@ -202,8 +203,6 @@ def apply(config): for member in members: i.add_member(interface=member) - i.set_state_up() - call_dependents() return None -- cgit v1.2.3 From 9e1c4f73a01ce27bb840072ee771da68593cbe08 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 27 Jan 2025 12:29:53 +0000 Subject: Add common interface state --- python/vyos/vpp/interface/bond.py | 30 +++++---------------- python/vyos/vpp/interface/interface.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 23 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py index a90f2950b..6a96bce58 100644 --- a/python/vyos/vpp/interface/bond.py +++ b/python/vyos/vpp/interface/bond.py @@ -15,11 +15,11 @@ # 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 +from vyos.vpp.interface.interface import Interface -class BondInterface: +class BondInterface(Interface): def __init__( self, ifname, @@ -27,14 +27,16 @@ class BondInterface: load_balance: int = 0, mac: str = '', kernel_interface: str = '', + state: str = 'up', ): + super().__init__(ifname) 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 - self.vpp = VPPControl() + self.state = state def add(self): """Create Bond interface @@ -55,6 +57,8 @@ class BondInterface: self.vpp.api.bond_create2(**create_args) if self.kernel_interface: self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + # Set interface state + self.set_state(self.state) def delete(self): """Delete Bond interface @@ -112,23 +116,3 @@ class BondInterface: a.kernel_delete() """ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) - - def set_state_up(self): - """Set Bond interface state to UP - Example: - from vyos.vpp.interface import BondInterface - a = BondInterface(ifname='bond0') - a.set_state_up() - """ - bond_if_index = self.vpp.get_sw_if_index(self.ifname) - self.vpp.api.sw_interface_set_flags(sw_if_index=bond_if_index, flags=1) - - def set_state_down(self): - """Set Bond interface state to DOWN - Example: - from vyos.vpp.interface import BondInterface - a = BondInterface(ifname='bond0') - a.set_state_down() - """ - bond_if_index = self.vpp.get_sw_if_index(self.ifname) - self.vpp.api.sw_interface_set_flags(sw_if_index=bond_if_index, flags=0) diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py index e69de29bb..33dc779c2 100644 --- a/python/vyos/vpp/interface/interface.py +++ b/python/vyos/vpp/interface/interface.py @@ -0,0 +1,49 @@ +# +# Copyright (C) 2025 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 + + +class Interface: + def __init__(self, ifname): + self.ifname = ifname + self.vpp = VPPControl() + + def set_state(self, state: str): + """Set interface state to UP or DOWN + Args: + state (str): The state of the interface. Options are 'up' and 'down'. + Example: + from vyos.vpp.interface import Interface + a = Interface(ifname='eth0') + a.set_state(state='up') + """ + if state not in ['up', 'down']: + raise ValueError(f"Invalid state: {state}") + state_flag = 1 if state == 'up' else 0 + if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.sw_interface_set_flags(sw_if_index=if_index, flags=state_flag) + + def get_state(self): + """Get interface state + Example: + from vyos.vpp.interface import Interface + a = Interface(ifname='eth0') + a.get_state() + """ + if_index = self.vpp.get_sw_if_index(self.ifname) + return self.vpp.api.sw_interface_dump(sw_if_index=if_index)[0]['flags'] -- cgit v1.2.3 From dfda2c7f305df94690cc4538a74ab213d25c579c Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 27 Jan 2025 18:01:57 +0000 Subject: Use common class for none ethernet interfaces state --- python/vyos/vpp/interface/__init__.py | 2 ++ python/vyos/vpp/interface/gre.py | 10 ++++++++-- python/vyos/vpp/interface/ipip.py | 17 ++++++++++++++--- python/vyos/vpp/interface/loopback.py | 11 +++++++---- python/vyos/vpp/interface/vxlan.py | 18 +++++++++++++++--- python/vyos/vpp/interface/xconnect.py | 2 ++ src/conf_mode/vpp_interfaces_gre.py | 5 ++++- src/conf_mode/vpp_interfaces_ipip.py | 3 ++- src/conf_mode/vpp_interfaces_loopback.py | 3 ++- src/conf_mode/vpp_interfaces_vxlan.py | 3 ++- src/conf_mode/vpp_interfaces_xconnect.py | 3 ++- 11 files changed, 60 insertions(+), 17 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py index 14659ed7b..2048e07ca 100644 --- a/python/vyos/vpp/interface/__init__.py +++ b/python/vyos/vpp/interface/__init__.py @@ -20,6 +20,7 @@ from .bridge import BridgeInterface from .ethernet import EthernetInterface from .geneve import GeneveInterface from .gre import GREInterface +from .interface import Interface from .ipip import IPIPInterface from .loopback import LoopbackInterface from .vxlan import VXLANInterface @@ -32,6 +33,7 @@ __all__ = [ 'EthernetInterface', 'GeneveInterface', 'GREInterface', + 'Interface', 'IPIPInterface', 'LoopbackInterface', 'VXLANInterface', diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py index 8f3dbebae..5bef280c6 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -17,6 +17,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface def show(): @@ -29,7 +30,7 @@ def show(): return vpp.api.gre_tunnel_dump() -class GREInterface: +class GREInterface(Interface): """ Class representing a GRE (Generic Routing Encapsulation) interface. @@ -64,6 +65,7 @@ class GREInterface: tunnel_type: str = 'l3', mode: str = 'point-to-point', kernel_interface: str = '', + state: str = 'up', ): """ Initialize a GREInterface instance. @@ -75,7 +77,9 @@ class GREInterface: mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'. tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. kernel_interface (str): The associated kernel interface. Defaults to an empty string. + state (str): The state of the interface. Defaults to 'up'. """ + super().__init__(ifname) self.instance = int(ifname.removeprefix('gre')) self.ifname = ifname self.src_address = source_address @@ -83,7 +87,7 @@ class GREInterface: self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type] self.mode = self.MODE_MAP[mode] self.kernel_interface = kernel_interface - self.vpp = VPPControl() + self.initial_state = state def add(self): """Create GRE interface @@ -103,6 +107,8 @@ class GREInterface: 'type': self.tunnel_type, }, ) + # Set interface state + self.set_state(self.initial_state) def delete(self): """Delete GRE interface diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py index 1aa1e22f2..4779a8292 100644 --- a/python/vyos/vpp/interface/ipip.py +++ b/python/vyos/vpp/interface/ipip.py @@ -17,6 +17,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface def show(): @@ -29,14 +30,22 @@ def show(): return vpp.api.ipip_tunnel_dump() -class IPIPInterface: - def __init__(self, ifname, source_address, remote, kernel_interface: str = ''): +class IPIPInterface(Interface): + def __init__( + self, + ifname, + source_address, + remote, + kernel_interface: str = '', + state: str = 'up', + ): + super().__init__(ifname) self.instance = int(ifname.removeprefix('ipip')) self.ifname = ifname self.src_address = source_address self.dst_address = remote self.kernel_interface = kernel_interface - self.vpp = VPPControl() + self.initial_state = state def add(self): """Create IPIP interface @@ -53,6 +62,8 @@ class IPIPInterface: 'instance': self.instance, }, ) + # Set interface state + self.set_state(self.initial_state) def delete(self): """Delete IPIP interface diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py index 8a5414d54..d417f627d 100644 --- a/python/vyos/vpp/interface/loopback.py +++ b/python/vyos/vpp/interface/loopback.py @@ -16,17 +16,18 @@ # 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.interface.interface import Interface -class LoopbackInterface: +class LoopbackInterface(Interface): """Interface Loopback""" - def __init__(self, ifname, kernel_interface: str = ''): + def __init__(self, ifname, kernel_interface: str = '', state: str = 'up'): + super().__init__(ifname) self.instance = int(ifname.removeprefix('lo')) self.ifname = f'loop{self.instance}' self.kernel_interface = kernel_interface - self.vpp = VPPControl() + self.initial_state = state def add(self): """Create Loopback interface @@ -39,6 +40,8 @@ class LoopbackInterface: self.vpp.api.create_loopback_instance( is_specified=True, user_instance=self.instance ) + # Set interface state + self.set_state(self.initial_state) def delete(self): """Delete Loopback interface diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py index 36206e8c4..eacc901dc 100644 --- a/python/vyos/vpp/interface/vxlan.py +++ b/python/vyos/vpp/interface/vxlan.py @@ -17,6 +17,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface def show(): @@ -29,17 +30,26 @@ def show(): return vpp.api.vxlan_tunnel_dump() -class VXLANInterface: +class VXLANInterface(Interface): """Interface VXLAN""" - def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''): + def __init__( + self, + ifname, + source_address, + remote, + vni, + kernel_interface: str = '', + state: str = 'up', + ): + super().__init__(ifname) 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 - self.vpp = VPPControl() + self.initial_state = state def add(self): """Create VXLAN interface @@ -59,6 +69,8 @@ class VXLANInterface: decap_next_index=1, is_l3=False, ) + # Set interface state + self.set_state(self.initial_state) def delete(self): """Delete VXLAN interface diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py index 41cf773d9..badece6e8 100644 --- a/python/vyos/vpp/interface/xconnect.py +++ b/python/vyos/vpp/interface/xconnect.py @@ -25,10 +25,12 @@ class XconnectInterface: self, ifname: str, members: list = [], + state: str = 'up', ): self.ifname = ifname self.members = members self.vpp = VPPControl() + self.initial_state = state def add_l2_xconnect(self): """Add l2 cross connect diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index 817345135..c8e84f169 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -167,7 +167,10 @@ def apply(config): kernel_interface = config.get('kernel_interface', '') mode = config.get('mode') tunnel_type = config.get('tunnel_type') - i = GREInterface(ifname, src_addr, dst_addr, tunnel_type, mode, kernel_interface) + state = 'up' if 'disable' not in config else 'down' + i = GREInterface( + ifname, src_addr, dst_addr, tunnel_type, mode, kernel_interface, state + ) i.add() # Add kernel-interface (LCP) if interface is not exist diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py index e15eaf5e5..3b03f91cd 100644 --- a/src/conf_mode/vpp_interfaces_ipip.py +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -159,7 +159,8 @@ def apply(config): 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) + state = 'up' if 'disable' not in config else 'down' + i = IPIPInterface(ifname, src_addr, dst_addr, kernel_interface, state) i.add() if 'kernel_interface' in config: diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py index a96c379a1..6aa3fc850 100644 --- a/src/conf_mode/vpp_interfaces_loopback.py +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -130,7 +130,8 @@ def apply(config): # Add interface kernel_interface = config.get('kernel_interface', '') - i = LoopbackInterface(ifname, kernel_interface) + state = 'up' if 'disable' not in config else 'down' + i = LoopbackInterface(ifname, kernel_interface, state) i.add() # Add kernel-interface (LCP) if interface is not exist diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py index de3114124..81bc34bea 100644 --- a/src/conf_mode/vpp_interfaces_vxlan.py +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -165,7 +165,8 @@ def apply(config): 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) + state = 'up' if 'disable' not in config else 'down' + i = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface, state) i.add() # Add kernel-interface (LCP) if interface is not exist diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py index 3659c6479..c323684a3 100644 --- a/src/conf_mode/vpp_interfaces_xconnect.py +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -124,7 +124,8 @@ def apply(config): # Add xconnect members = config.get('member', {}).get('interface') - i = XconnectInterface(ifname, members=members) + state = 'up' if 'disable' not in config else 'down' + i = XconnectInterface(ifname, members=members, state=state) i.add_l2_xconnect() return None -- cgit v1.2.3 From 2d5462f4a024469629f6fb2c5286e7f45d6a6df1 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Wed, 29 Jan 2025 15:27:42 +0200 Subject: Use lcp_nl_resync instead of lcp_resync for control_vpp.py Use lcp_nl_resync (available with patch 04) 0004-Improved-lcp-resync.patch It improves sync speed and should fix or smoketest issues related to connecting to VPP API speed --- python/vyos/vpp/control_vpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 39ecac906..2c81b2db2 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -283,7 +283,7 @@ class VPPControl: 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() + return self.__vpp_api_client.api.lcp_nl_resync() @_Decorators.check_retval @_Decorators.api_call -- cgit v1.2.3 From 13f99beada6ac28ede7c74d434b84a63bf9905b0 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Wed, 19 Feb 2025 15:19:09 +0000 Subject: T7181: VPP add initial source NAT implentation Add initial source NAT implementation ``` set vpp nat44 source inbound-interface 'eth2' set vpp nat44 source outbound-interface 'eth1' set vpp nat44 source translation address '192.0.2.1-192.0.2.2' ``` Add initial simple implementation of the source NAT In the future, we'll extend it to the rules if it is possible to do via VPP API --- data/config-mode-dependencies/vyos-vpp.json | 8 ++ data/templates/vpp/startup.conf.j2 | 4 +- interface-definitions/vpp.xml.in | 65 ++++++++++++ python/vyos/vpp/nat/__init__.py | 3 + python/vyos/vpp/nat/nat44.py | 151 ++++++++++++++++++++++++++++ src/conf_mode/vpp.py | 4 + src/conf_mode/vpp_nat_source.py | 119 ++++++++++++++++++++++ 7 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 python/vyos/vpp/nat/nat44.py create mode 100644 src/conf_mode/vpp_nat_source.py (limited to 'python') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 04dc4a771..50e465e80 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -10,34 +10,42 @@ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"], "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ethernet": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_geneve": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_gre": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ipip": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_loopback": { + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_vxlan": { "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_nat_source": ["vpp_nat_source"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] } } diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index cf10b6ce5..c3bfc2b51 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -94,8 +94,8 @@ plugins { 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 nat_plugin.so { enable } + plugin nat44_ei_plugin.so { enable } # plugin nat44_ei_plugin.so { enable } # plugin nat64_plugin.so { enable } # plugin nat66_plugin.so { enable } diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 1f1cb44cc..175b28a0b 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -833,6 +833,71 @@ + + + NAT44 + + + + + Source NAT setting + 320 + + + + + Inbound interface of NAT traffic + + any + + + + + + + Outbound interface of NAT traffic + + any + + + + + + + Outside NAT IP (source NAT only) + + + + + IP address, subnet, or range + + masquerade + + + ipv4 + IPv4 address to match + + + ipv4range + IPv4 address range to match + + + masquerade + NAT to the primary address of outbound-interface + + + + + (masquerade) + + + + + + + + + VPP kernel interface settings diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py index e69de29bb..bbd011435 100644 --- a/python/vyos/vpp/nat/__init__.py +++ b/python/vyos/vpp/nat/__init__.py @@ -0,0 +1,3 @@ +from .nat44 import Nat44 + +__all__ = ['Nat44'] diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py new file mode 100644 index 000000000..47d0ced1d --- /dev/null +++ b/python/vyos/vpp/nat/nat44.py @@ -0,0 +1,151 @@ +# +# Copyright (C) 2025 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 + + +class Nat44: + def __init__( + self, + interface_in: str, + interface_out: str, + translation_pool: str, + ): + self.interface_in = interface_in + self.interface_out = interface_out + self.translation_pool = translation_pool + self.vpp = VPPControl() + + def enable_nat44_ed(self): + """Enable NAT44 endpoint dependent plugin + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.enable_nat44_ed() + https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/nat44-ed/nat44_ed.api + """ + self.vpp.api.nat44_ed_plugin_enable_disable(enable=True) + + def enable_nat44_ei(self): + """Enable NAT44 endpoint independent plugin + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.enable_nat44_ei() + """ + self.vpp.api.nat44_ei_plugin_enable_disable(enable=True) + + def enable_nat44_forwarding(self): + """Enable NAT44 forwarding + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.enable_nat44_forwarding() + """ + self.vpp.api.nat44_forwarding_enable_disable(enable=True) + + def disable_nat44_forwarding(self): + """Disable NAT44 forwarding + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.disable_nat44_forwarding() + """ + self.vpp.api.nat44_forwarding_enable_disable(enable=False) + + def add_nat44_out_interface(self): + """Add NAT44 output interface + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44('eth0') + nat44.add_nat44_out_interface() + """ + self.vpp.api.nat44_ed_add_del_output_interface( + sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + is_add=True, + ) + + def delete_nat44_out_interface(self): + """Delete NAT44 output interface""" + self.vpp.api.nat44_ed_add_del_output_interface( + sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + is_add=False, + ) + + # needs to check + def add_nat44_interface_inside(self): + """Add NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=1, + sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + is_add=True, + ) + + # needs to check + def delete_nat44_interface_inside(self): + """Delete NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=1, + sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + is_add=False, + ) + + # needs to check + def add_nat44_interface_outside(self): + """Add NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=0, + sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + is_add=True, + ) + + # needs to check + def delete_nat44_interface_outside(self): + """Delete NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=0, + sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + is_add=False, + ) + + def add_nat44_address_range(self): + """Add NAT44 address range""" + if '-' not in self.translation_pool and self.translation_pool != 'masquerade': + first_ip_address = last_ip_address = self.translation_pool + else: + first_ip_address, last_ip_address = self.translation_pool.split('-') + self.vpp.api.nat44_add_del_address_range( + first_ip_address=first_ip_address, + last_ip_address=last_ip_address, + is_add=True, + ) + + def delete_nat44_address_range(self): + """Delete NAT44 address range""" + if '-' not in self.translation_pool and self.translation_pool != 'masquerade': + first_ip_address = last_ip_address = self.translation_pool + else: + first_ip_address, last_ip_address = self.translation_pool.split('-') + self.vpp.api.nat44_add_del_address_range( + first_ip_address=first_ip_address, + last_ip_address=last_ip_address, + is_add=False, + ) + + def enable_ipfix(self): + """Enable NAT44 IPFIX logging""" + self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index ec6873e0d..f0eb3a0bf 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -114,6 +114,10 @@ def get_config(config=None): # to be reinitialized after the commit set_dependents('ethernet', conf, removed_iface) + # NAT dependency + if conf.exists(['vpp', 'nat44', 'source']): + set_dependents('vpp_nat_source', conf) + if not conf.exists(base): return { 'removed_ifaces': removed_ifaces, diff --git a/src/conf_mode/vpp_nat_source.py b/src/conf_mode/vpp_nat_source.py new file mode 100644 index 000000000..2e1884c3a --- /dev/null +++ b/src/conf_mode/vpp_nat_source.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 +from vyos import ConfigError +from vyos.vpp.nat.nat44 import Nat44 + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat44', 'source'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base, + 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}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + required_keys = {'inbound_interface', 'outbound_interface'} + 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('_', '-')}" + ) + + if not config.get('translation', {}).get('address'): + raise ConfigError('Translation requires address') + + if config.get('translation', {}).get('address') == 'masquerade': + raise ConfigError('Masquerade is not implemented') + + +def generate(config): + pass + + +def apply(config): + # Delete NAT source + if 'effective' in config: + remove_config = config.get('effective') + interface_in = remove_config.get('inbound_interface') + interface_out = remove_config.get('outbound_interface') + translation_address = remove_config.get('translation', {}).get('address') + + n = Nat44(interface_in, interface_out, translation_address) + n.delete_nat44_out_interface() + n.delete_nat44_interface_inside() + n.delete_nat44_address_range() + + if 'remove' in config: + return None + + # Add NAT44 + interface_in = config.get('inbound_interface') + interface_out = config.get('outbound_interface') + translation_address = config.get('translation', {}).get('address') + + n = Nat44(interface_in, interface_out, translation_address) + n.enable_nat44_ed() + n.enable_nat44_forwarding() + n.add_nat44_out_interface() + # n.add_nat44_interface_inside() + n.add_nat44_address_range() + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From 4abb970491dfc83234b817808d18ae6bfeeeffe8 Mon Sep 17 00:00:00 2001 From: khramshinr Date: Thu, 20 Feb 2025 14:08:54 +0700 Subject: T7073: Verify VPP buffers page size T7077: Verify VPP memory default-hugepage-size T7079: Verify VPP memory main-heap-page-size T7080: Verify VPP statseg page-size get available hugepage sizes align memory main-heap-size by page size validate host_resources max_map_count --- .../include/unformat_log2_page_size.xml.i | 16 +--- interface-definitions/vpp.xml.in | 4 +- python/vyos/vpp/utils.py | 94 ++++++++++++++++++++++ smoketest/scripts/cli/test_vpp.py | 76 +++++++++++++++-- src/completion/list_mem_page_size.py | 69 ++++++++++++++++ src/conf_mode/vpp.py | 47 +++++++---- 6 files changed, 269 insertions(+), 37 deletions(-) create mode 100644 src/completion/list_mem_page_size.py (limited to 'python') diff --git a/interface-definitions/include/unformat_log2_page_size.xml.i b/interface-definitions/include/unformat_log2_page_size.xml.i index 240fd5bb8..ce6f70dc8 100644 --- a/interface-definitions/include/unformat_log2_page_size.xml.i +++ b/interface-definitions/include/unformat_log2_page_size.xml.i @@ -1,6 +1,7 @@ default default-hugepage + default @@ -10,20 +11,7 @@ default-hugepage Default huge-page - - <number>K - Kilobyte - - - <number>M - Megabyte - - - <number>G - Gigabyte - - - (default|default-hugepage|\d+K|\d+M|\d+G) + (default|default-hugepage|4K|8K|1024K|64K|256K|2048K|4096K|16384K|262144K|1048576K|16777216K|1M|2M|4M|16M|256M|1024M|16384M|1G|16G) diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 1f1cb44cc..e0784fc43 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -774,7 +774,9 @@ Default hugepage size - #include + + + diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 08d69bba2..681d25578 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -16,12 +16,16 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import ctypes +import os import socket from fcntl import ioctl from pathlib import Path from struct import pack +mem_shift = {'K': 10, 'k': 10, 'M': 20, 'm': 20, 'G': 30, 'g': 30} + + def iftunnel_transform(iface: str) -> str: """Transform interface name from `xxxNN` to `xxx_tunnelNN` @@ -211,6 +215,96 @@ def cli_ifaces_lcp_kernel_list( return lcp_kernel_ifaces +def get_default_hugepage_size() -> int: + """ + Retrieve the system's default huge page size. + :return: The default huge page size in bytes. + """ + page_size = None + try: + # default huge page size + memfd = os.memfd_create('tmp', os.MFD_HUGETLB) + st = os.fstat(memfd) + page_size = st.st_blksize + os.close(memfd) + except OSError: + pass + + return page_size + + +def get_default_page_size() -> int: + """ + Retrieve the system's default page size. + :return: The default page size in bytes. + """ + return os.sysconf('SC_PAGESIZE') + + +def get_hugepage_sizes() -> list[int]: + """ + Retrieve all available huge page sizes from the system. + :return: A list of huge page sizes in bytes. + """ + huge_sizes = [] + path = '/sys/kernel/mm/hugepages/' + try: + entries = os.listdir(path) + for entry in entries: + if entry.startswith('hugepages-'): + try: + size_kb = int(entry.replace('hugepages-', '').replace('kB', '')) + huge_sizes.append(size_kb << 10) # Convert KB to bytes + except ValueError: + pass + except FileNotFoundError: + pass + + return huge_sizes + + +def human_memory_to_bytes(value: str) -> int: + """ + Convert a human-readable vpp memory format (K, M, G) to a byte value. + + :param value: The string memory size in vpp human-readable format. + :return: A int representing the value. + """ + try: + return int(value) + except ValueError: + return int(value[:-1]) << mem_shift[value[-1]] + + +def bytes_to_human_memory(value: int, unit: str) -> str | None: + """ + Convert a byte value to a human-readable format (K, M, G). + + :param value: The size in bytes. + :param unit: The unit to convert to ('K', 'M', 'G'). + :return: A string representing the value in the specified unit, or None if zero. + """ + val = value >> mem_shift[unit] + return f'{val}{unit}' if val else None + + +def human_page_memory_to_bytes(value: str) -> int: + """ + Convert a human-readable vpp page size format to a byte value. + + :param value: The string memory size in vpp human-readable format. + :return: A int representing the value. + """ + default = { + 'default': get_default_page_size, + 'default-hugepage': get_default_hugepage_size, + } + try: + return default[value]() + except KeyError: + return human_memory_to_bytes(value) + + class EthtoolGDrvinfo: """Return interface details like `ethtol -i` does""" diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 86b00da44..9f28c970f 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -19,7 +19,9 @@ import os import re +import sys import unittest +from collections import defaultdict from json import loads @@ -30,6 +32,9 @@ from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.utils.process import rc_cmd +sys.path.append(os.getenv('vyos_completion_dir')) +from list_mem_page_size import list_mem_page_size + PROCESS_NAME = 'vpp_main' VPP_CONF = '/run/vpp/vpp.conf' base_path = ['vpp'] @@ -37,6 +42,38 @@ driver = 'dpdk' interface = 'eth1' +def get_vpp_config(): + config = defaultdict(dict) + current_section = None + + with open(VPP_CONF, 'r') as f: + for line in f: + line = line.strip() + + if not line or line.startswith('#'): # Ignore empty lines and comments + continue + + section_match = re.match(r'([a-zA-Z0-9_-]+)\s*{', line) + if section_match: + current_section = section_match.group(1) + config[current_section] = {} + continue + + if line == '}': # End of section + current_section = None + continue + + key_value_match = re.match(r'([a-zA-Z0-9_-]+)\s+(.+)', line) + if key_value_match: + key, value = key_value_match.groups() + if current_section: + config[current_section][key] = value + else: + config[key] = value + + return config + + def get_address(interface): rc, data = rc_cmd(f'ip --json address show dev {interface}') if rc == 0: @@ -60,12 +97,13 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): 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() + try: + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + finally: + # Ensure these cleanup operations always run + self.cli_delete(base_path) + self.cli_commit() self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) @@ -1064,6 +1102,32 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): for config_entry in config_entries: self.assertIn(config_entry, config) + def test_13_mem_page_size(self): + sizes = ['default', 'default-hugepage'] + list_mem_page_size() + for size in sizes: + self.cli_set(base_path + ['settings', 'buffers', 'page-size', size]) + self.cli_set(base_path + ['settings', 'statseg', 'page-size', size]) + self.cli_set( + base_path + ['settings', 'memory', 'main-heap-page-size', size] + ) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['buffers']['page-size'], size) + self.assertEqual(conf['statseg']['page-size'], size) + self.assertEqual(conf['memory']['main-heap-page-size'], size) + + def test_14_mem_default_hugepage(self): + sizes = list_mem_page_size(hugepage_only=True) + for size in sizes: + self.cli_set( + base_path + ['settings', 'memory', 'default-hugepage-size', size] + ) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['memory']['default-hugepage-size'], size) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/completion/list_mem_page_size.py b/src/completion/list_mem_page_size.py new file mode 100644 index 000000000..1ed81a15f --- /dev/null +++ b/src/completion/list_mem_page_size.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 . +import argparse +from vyos.vpp.utils import ( + get_hugepage_sizes, + get_default_hugepage_size, + get_default_page_size, + bytes_to_human_memory, +) + + +def get_default_page_sizes() -> list[int]: + """ + Retrieve the system's default page sizes, including huge pages. + :return: A list of page sizes in bytes. + """ + page_sizes = [] + # default system page size + page_size = get_default_page_size() + if page_size: + page_sizes.append(page_size) + + # default huge page size + page_size = get_default_hugepage_size() + if page_size: + page_sizes.append(page_size) + + return page_sizes + + +def list_mem_page_size(hugepage_only=None) -> list[str]: + result = [] + page_sizes = get_hugepage_sizes() + + if not hugepage_only: + page_sizes += get_default_page_sizes() + + page_sizes = set(page_sizes) + for unit in ['K', 'M', 'G']: + for size in page_sizes: + if val := bytes_to_human_memory(size, unit): + result.append(val) + + return result + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--hugepage_only', type=str, help='List only available hugepage sizes.' + ) + args = parser.parse_args() + + result = list_mem_page_size(args.hugepage_only) + print(' '.join(result)) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index ec6873e0d..aea0c1754 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -39,7 +39,11 @@ 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.utils import ( + EthtoolGDrvinfo, + human_page_memory_to_bytes, + human_memory_to_bytes, +) from vyos.vpp.configdb import JSONStorage airbag.enable() @@ -252,18 +256,6 @@ def get_config(config=None): 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() @@ -283,12 +275,24 @@ def verify_memory(settings): ) memory_required += netlink_buffer_size - memory_main_heap = convert_to_int( + memory_main_heap = human_memory_to_bytes( 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_main_heap_page_size = settings.get('memory', {}).get( + 'main_heap_page_size', 0 + ) + if memory_main_heap_page_size: + memory_main_heap_page_size = human_page_memory_to_bytes( + memory_main_heap_page_size + ) + memory_required += (memory_main_heap + memory_main_heap_page_size - 1) & ~( + memory_main_heap_page_size - 1 + ) + else: + memory_required += memory_main_heap + + statseg_size = human_memory_to_bytes(settings.get('statseg', {}).get('size', '96M')) memory_required += statseg_size if memory_available < memory_required: @@ -452,6 +456,17 @@ def verify(config): raise ConfigError('"cpu corelist-workers" is not correct') verify_memory(config['settings']) + if 'host_resources' in config['settings']: + if ( + 'nr_hugepages' in config['settings']['host_resources'] + and 'max_map_count' in config['settings']['host_resources'] + ): + if int(config['settings']['host_resources']['max_map_count']) < 2 * int( + config['settings']['host_resources']['nr_hugepages'] + ): + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' + ) # Check if deleted interfaces are not xconnect memebrs for iface_config in config.get('removed_ifaces', []): -- cgit v1.2.3 From 47a8502fcdc4a371742b52176074af91debd66a7 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 24 Feb 2025 19:24:46 +0000 Subject: vd-275: Add loopback bvi interface for a bridge member Allow to configure VPP loopback interface as BVI interface In the VPP a bridge-domain is the L2 bridge and does not have its own interface Loopback is required if we want to ping from/to the bridge. ``` set vpp interfaces bridge br10 member interface lo23 bvi ``` --- interface-definitions/vpp.xml.in | 13 +++++++++--- python/vyos/vpp/interface/bridge.py | 5 +++-- smoketest/scripts/cli/test_vpp.py | 29 +++++++++++++++++++++++-- src/conf_mode/vpp_interfaces_bridge.py | 39 +++++++++++++++++++++++++++------- 4 files changed, 71 insertions(+), 15 deletions(-) (limited to 'python') diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index e0784fc43..a7c293c94 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -132,7 +132,7 @@ Bridge member interfaces - + Member interface name @@ -142,9 +142,16 @@ txt Interface name - - + + + + Bridge Virtual Interface (BVI) + + + + + diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py index 407eab08c..5ebe46d45 100644 --- a/python/vyos/vpp/interface/bridge.py +++ b/python/vyos/vpp/interface/bridge.py @@ -70,7 +70,7 @@ class BridgeInterface: """ self.vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix) - def add_member(self, member: str | int): + def add_member(self, member: str | int, port_type: int = 0): """Add member to Bridge interface Attaches a VPP interface to the Bridge interface specified by `interface_suffix`. @@ -80,6 +80,7 @@ class BridgeInterface: Args: member (str or int): The name or index of the VPP network interface to be added as a member to the bridge. + port_type: 0 - Normal port, 1 - BVI port Example: from vyos.vpp.interface import BridgeInterface @@ -96,7 +97,7 @@ class BridgeInterface: member_if_index = self.vpp.get_sw_if_index(member) return self.vpp.api.sw_interface_set_l2_bridge( - rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=0 + rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=port_type ) def detach_member(self, member: str | int): diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 69f1410ab..426d19884 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -740,7 +740,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') # Set non exist member - # expect raise ConfigErro + # expect raise ConfigError self.cli_set( base_path + [ @@ -799,7 +799,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): # 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('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) @@ -819,6 +819,31 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+') + # Add Loopback BVI to the bridge + self.cli_set(base_path + ['interfaces', 'loopback', f'lo{vni}']) + self.cli_set( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + f'lo{vni}', + 'bvi', + ] + ) + # 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) + + self.assertIn('10 1 0 off', normalized_out) + self.assertRegex(out, r'\bloop23\s+\d+\s+\d+\s+\d+\s+\*\s+') + def test_08_vpp_ipip(self): interface_ipip = 'ipip12' interface_kernel = 'vpptun12' diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py index e7a2427f9..d98067a6a 100644 --- a/src/conf_mode/vpp_interfaces_bridge.py +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -19,7 +19,7 @@ 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.vpp.interface import BridgeInterface from vyos.vpp.utils import iftunnel_transform @@ -77,7 +77,7 @@ def get_config(config=None) -> dict: ) # determine which members have been removed - interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) + interfaces_removed = node_changed(conf, base + [ifname, 'member', 'interface']) if interfaces_removed: config['members_removed'] = interfaces_removed @@ -92,19 +92,30 @@ def verify(config): # Check if interface exists in vpp before adding to bridge-domain - allowed_prefixes = ('gre', 'geneve', 'vxlan') + allowed_prefixes = ('gre', 'geneve', 'lo', '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 + bvi_exists = False + for member, member_config in ( + config.get('member', {}).get('interface', {}).items() + ): + # Check if the interface exists in VPP settings or starts with allowed prefixes if not ( - member in config.get('vpp_interfaces', []) + 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}" ) + # Check if BVI is already defined, only one BVI per bridge domain is allowed + if 'bvi' in member_config: + if bvi_exists: + raise ConfigError("Only one BVI per bridge domain is allowed") + if not member.startswith('lo'): + raise ConfigError("BVI can only be defined on loopback interface") + bvi_exists = True + def generate(config): pass @@ -120,6 +131,9 @@ def apply(config): for member in config.get('members_removed'): if member.startswith(interface_transform_filter): member = iftunnel_transform(member) + if member.startswith('lo'): + # interface name in VPP is loopX + member = member.replace('lo', 'loop') i.detach_member(member=member) # Delete bridge domain @@ -138,10 +152,19 @@ def apply(config): # Add members to bridge if members: br = BridgeInterface(ifname) - for member in members: + port_type = 0 + for member, member_config in members.items(): if member.startswith(interface_transform_filter): member = iftunnel_transform(member) - br.add_member(member=member) + if member.startswith('lo'): + # interface name in VPP is loopX + member = member.replace('lo', 'loop') + if 'bvi' in member_config: + port_type = 1 + + br.add_member(member=member, port_type=port_type) + # set default port type 0 (not BVI) + port_type = 0 return None -- cgit v1.2.3 From 877166b8aef1132f5bad208aee00444819264501 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Mon, 3 Mar 2025 15:41:27 +0200 Subject: T7189: VPP source of the tunnel interface should be checked and configured Changed priority for VPP interfaces: all VPP interfaces must be configured after Ethernet interfaces --- interface-definitions/vpp.xml.in | 18 +++++----- python/vyos/vpp/config_verify.py | 13 +++++++ python/vyos/vpp/utils.py | 37 +++++++++++++++++++ smoketest/scripts/cli/test_vpp.py | 67 ++++++++++++++++++++++++++++++++--- src/conf_mode/vpp_interfaces_gre.py | 11 +++++- src/conf_mode/vpp_interfaces_ipip.py | 11 +++++- src/conf_mode/vpp_interfaces_vxlan.py | 11 +++++- 7 files changed, 151 insertions(+), 17 deletions(-) (limited to 'python') diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 150803a0d..e9075588e 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -14,7 +14,7 @@ - 304 + 324 Bonding Interface/Link Aggregation bond[0-9]+ @@ -112,7 +112,7 @@ - 307 + 327 Bridge domain @@ -159,7 +159,7 @@ - 305 + 325 Generic Network Encapsulation (GRE) Interface gre[0-9]+ @@ -246,7 +246,7 @@ - 305 + 325 IP encapsulation tunnel interface ipip[0-9]+ @@ -267,7 +267,7 @@ - 305 + 325 Loopback Interface lo[0-9]+ @@ -286,7 +286,7 @@ - 305 + 325 Virtual Extensible LAN (VXLAN) Interface vxlan[0-9]+ @@ -309,7 +309,7 @@ Layer 2 cross connect - 305 + 325 xcon[0-9]+ @@ -845,7 +845,7 @@ VPP kernel interface settings - 308 + 328 vpptapN Kernel interface name diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 6b35858d2..830d4af73 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -102,6 +102,19 @@ def verify_vpp_remove_xconnect_interface(config: dict): ) +def verify_vpp_tunnel_source_address(config: dict): + from vyos.utils.network import is_intf_addr_assigned + + address = config.get('source_address') + for iface in config.get('vpp_ether_vif_ifaces', []): + if is_intf_addr_assigned(iface, address): + return True + + raise ConfigError( + f'Source address "{address}" is not assigned on any Ethernet or VIF interface!' + ) + + def verify_dev_driver(iface_name: str, driver_type: str) -> bool: # Lists of drivers compatible with DPDK and XDP drivers_dpdk: list[str] = [ diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 681d25578..fe08a98b8 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -85,6 +85,43 @@ def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]: return vpp_ifaces +def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]: + """List of all VPP Ethernet interfaces with VIFs + + Args: + config_instance (VyOS Config): VyOS Config instance + + Returns: + list[str]: list of interfaces + """ + from vyos.configdict import get_interface_dict + + # Read a config + config = config_instance.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + ifaces: list[str] = [] + + # Get a list of Ethernet interfaces + for iface in config.get('settings', {}).get('interface', {}).keys(): + ifaces.append(iface) + + # Add Ethernet interfaces with VIFs + for iface in ifaces: + _, iface_config = get_interface_dict( + config_instance, ['interfaces', 'ethernet'], ifname=iface + ) + ifaces.extend([f'{iface}.{vif}' for vif in iface_config.get('vif', {})]) + ifaces.extend([f'{iface}.{vif_s}' for vif_s in iface_config.get('vif_s', {})]) + + return ifaces + + def vpp_ifaces_list(vpp_api) -> list[dict]: """List interfaces in VPP diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 6c19fbba0..57f47067a 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -106,6 +106,10 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + # delete address for Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'address']) + self.cli_commit() + self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) @@ -170,11 +174,21 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): base_path + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address] ) + self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni]) + + # remote and source address must not be the same + # expect raise ConfigError + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'remote', source_address] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + 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 + [ @@ -214,6 +228,23 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): new_source_address, ] ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + [ + 'interfaces', + 'ethernet', + interface, + 'vif', + vni, + 'address', + f'{new_source_address}/24', + ] + ) self.cli_commit() # check gre interface after update @@ -274,7 +305,11 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): 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_delete(base_path + ['interfaces', 'vxlan', interface_vxlan]) + self.cli_commit() + + # delete vif Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'vif']) self.cli_commit() def test_03_vpp_gre(self): @@ -302,6 +337,15 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31'] ) + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{source_address}/24'] + ) + # commit changes self.cli_commit() @@ -319,6 +363,10 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): base_path + ['interfaces', 'gre', interface_gre, 'source-address', new_source_address] ) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) self.cli_commit() # check gre interface after update @@ -364,7 +412,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): 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_delete(base_path + ['interfaces', 'gre', interface_gre]) self.cli_commit() @unittest.skip('Skipping this test geneve index always is 0') @@ -566,7 +614,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): 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_delete(base_path + ['interfaces', 'loopback', interface_loopback]) self.cli_commit() @unittest.skip('Skipping temporary bonding, sometimes get recursion T7117') @@ -900,6 +948,15 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): new_source_address, ] ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) self.cli_commit() # check ipip interface after update @@ -951,7 +1008,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): 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_delete(base_path + ['interfaces', 'ipip', interface_ipip]) self.cli_commit() def test_09_vpp_xconnect(self): diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index de7398248..823f3ff4d 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -32,8 +32,9 @@ from vyos.vpp.config_verify import ( verify_vpp_change_kernel_interface, verify_vpp_remove_xconnect_interface, verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, ) -from vyos.vpp.utils import cli_ifaces_lcp_kernel_list +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces def get_config(config=None) -> dict: @@ -94,6 +95,9 @@ def get_config(config=None) -> dict: # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + # Dependency config['xconn_members'] = deps_xconnect_dict(conf) if ifname in config['xconn_members']: @@ -130,6 +134,11 @@ def verify(config): f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" ) + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + # check multipoint mode if config.get('mode') == 'point-to-multipoint': # For multipoint mode, remote IP must be 0.0.0.0 diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py index 2428a9850..c52e7c997 100644 --- a/src/conf_mode/vpp_interfaces_ipip.py +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -31,8 +31,9 @@ from vyos.vpp.config_verify import ( verify_vpp_change_kernel_interface, verify_vpp_remove_xconnect_interface, verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, ) -from vyos.vpp.utils import cli_ifaces_lcp_kernel_list +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces def get_config(config=None) -> dict: @@ -93,6 +94,9 @@ def get_config(config=None) -> dict: # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + # Dependency config['xconn_members'] = deps_xconnect_dict(conf) if ifname in config['xconn_members']: @@ -130,6 +134,11 @@ def verify(config): f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" ) + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + # 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' diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py index b94b39d6d..963cd90ec 100644 --- a/src/conf_mode/vpp_interfaces_vxlan.py +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -33,8 +33,9 @@ from vyos.vpp.config_verify import ( verify_vpp_change_kernel_interface, verify_vpp_remove_xconnect_interface, verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, ) -from vyos.vpp.utils import cli_ifaces_lcp_kernel_list +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces def get_config(config=None) -> dict: @@ -95,6 +96,9 @@ def get_config(config=None) -> dict: # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + # Dependency config['xconn_members'] = deps_xconnect_dict(conf) if ifname in config['xconn_members']: @@ -134,6 +138,11 @@ def verify(config): f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" ) + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + # 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' -- cgit v1.2.3 From 07a3b0f5ae87a2ab400390c9fa0ca632d7815e15 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:30:12 +0300 Subject: T7283: VPP add static NAT support (#24) * T7283: VPP add static NAT support Add static mapping NAT implementation ``` set vpp nat44 static rule 10 outbound-interface 'eth0' set vpp nat44 static rule 10 inbound-interface 'eth1' set vpp nat44 static rule 10 destination address 192.168.122.10 # optional, if not set outbound interface ip address is used set vpp nat44 static rule 10 destination port 6545 # optional set vpp nat44 static rule 10 protocol tcp|udp|icmp|all # optional, defaults to "all" set vpp nat44 static rule 10 translation address 100.64.0.10 set vpp nat44 static rule 10 translation port 64010 # optional ``` * Improve help strings (Daniil Baturin) --------- Co-authored-by: Daniil Baturin --- data/config-mode-dependencies/vyos-vpp.json | 2 + interface-definitions/vpp.xml.in | 105 ++++++++++++++ op-mode-definitions/show_vpp_nat44.xml.in | 36 +++++ python/vyos/vpp/control_vpp.py | 15 ++ python/vyos/vpp/nat/nat44.py | 81 +++++++++-- src/conf_mode/vpp.py | 2 + src/conf_mode/vpp_nat_static.py | 209 ++++++++++++++++++++++++++++ src/op_mode/show_vpp_nat44.py | 183 ++++++++++++++++++++++++ 8 files changed, 625 insertions(+), 8 deletions(-) create mode 100644 op-mode-definitions/show_vpp_nat44.xml.in create mode 100644 src/conf_mode/vpp_nat_static.py create mode 100644 src/op_mode/show_vpp_nat44.py (limited to 'python') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 50e465e80..307e763b9 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -11,6 +11,7 @@ "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat_static": ["vpp_nat_static"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { @@ -49,3 +50,4 @@ "vpp_kernel_interface": ["vpp_kernel-interfaces"] } } + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 94d87a8dd..66a38f546 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -905,6 +905,111 @@ + + + Static NAT settings + 321 + + + + + Rule number for NAT + + u32 + Number of NAT rule + + + + + + NAT external parameters + + + + + IP address + + ipv4 + IPv4 address + + + + + + + #include + + + + + Protocol to NAT + + tcp udp icmp all + + + all + All protocols (TCP, UDP, and ICMP) + + + icmp + Internet control message protocol + + + tcp + Transmission control protocol + + + udp + user datagram protocol + + + (tcp|udp|icmp|all) + + + all + + + + Outbound interface of NAT traffic + + any + + + + + + + Inbound interface of NAT traffic + + any + + + + + + + NAT internal parameters + + + + + IP address + + ipv4 + IPv4 address + + + + + + + #include + + + + + + diff --git a/op-mode-definitions/show_vpp_nat44.xml.in b/op-mode-definitions/show_vpp_nat44.xml.in new file mode 100644 index 000000000..f2e77267a --- /dev/null +++ b/op-mode-definitions/show_vpp_nat44.xml.in @@ -0,0 +1,36 @@ + + + + + + + + + Show VPP NAT44 information + + + + + Show VPP NAT44 static mapping + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_static + + + + Show VPP NAT44 sessions + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_sessions + + + + Show VPP NAT44 summary + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_summary + + + + + + + + diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 2c81b2db2..9d45bbd91 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -160,6 +160,21 @@ class VPPControl: return iface.sw_if_index return None + @_Decorators.api_call + def get_interface_name(self, index: int) -> str | None: + """Find interface name by interface index in VPP + + Args: + index (int): interface index inside VPP + + Returns: + str | None: Interface name or None (if was not found) + """ + for iface in self.__vpp_api_client.api.sw_interface_dump(): + if iface.sw_if_index == index: + return iface.interface_name + return None + @_Decorators.check_retval @_Decorators.api_call def lcp_pair_add( diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index 47d0ced1d..985e5a583 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -86,38 +86,34 @@ class Nat44: is_add=False, ) - # needs to check def add_nat44_interface_inside(self): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=1, + flags=0x20, sw_if_index=self.vpp.get_sw_if_index(self.interface_in), is_add=True, ) - # needs to check def delete_nat44_interface_inside(self): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=1, + flags=0x20, sw_if_index=self.vpp.get_sw_if_index(self.interface_in), is_add=False, ) - # needs to check def add_nat44_interface_outside(self): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0, + flags=0x10, sw_if_index=self.vpp.get_sw_if_index(self.interface_out), is_add=True, ) - # needs to check def delete_nat44_interface_outside(self): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0, + flags=0x10, sw_if_index=self.vpp.get_sw_if_index(self.interface_out), is_add=False, ) @@ -149,3 +145,72 @@ class Nat44: def enable_ipfix(self): """Enable NAT44 IPFIX logging""" self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) + + +class Nat44Static(Nat44): + def __init__(self): + self.vpp = VPPControl() + + def add_inbound_interface(self, interface_in): + self.interface_in = interface_in + self.add_nat44_interface_inside() + + def delete_inbound_interface(self, interface_in): + self.interface_in = interface_in + self.delete_nat44_interface_inside() + + def add_outbound_interface(self, interface_out): + self.interface_out = interface_out + self.add_nat44_interface_outside() + + def delete_outbound_interface(self, interface_out): + self.interface_out = interface_out + self.delete_nat44_interface_outside() + + def add_nat44_static_mapping( + self, + iface_out, + local_ip, + external_ip, + local_port, + external_port, + protocol, + use_iface, + ): + """Add NAT44 static mapping""" + self.vpp.api.nat44_add_del_static_mapping_v2( + local_ip_address=local_ip, + external_ip_address=external_ip, + protocol=protocol, + local_port=local_port, + external_port=external_port, + flags=0x08 if not (protocol or local_port) else 0x00, + external_sw_if_index=( + self.vpp.get_sw_if_index(iface_out) if use_iface else 0xFFFFFFFF + ), + is_add=True, + ) + + def delete_nat44_static_mapping( + self, + iface_out, + local_ip, + external_ip, + local_port, + external_port, + protocol, + use_iface, + ): + """Delete NAT44 static mapping""" + self.vpp.api.nat44_add_del_static_mapping_v2( + local_ip_address=local_ip, + external_ip_address=external_ip, + protocol=protocol, + local_port=local_port, + external_port=external_port, + flags=0x08 if not (protocol or local_port) else 0x00, + external_sw_if_index=( + self.vpp.get_sw_if_index(iface_out) if use_iface else 0xFFFFFFFF + ), + is_add=False, + ) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 1f4c6c0f0..ca7d19160 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -138,6 +138,8 @@ def get_config(config=None): # NAT dependency if conf.exists(['vpp', 'nat44', 'source']): set_dependents('vpp_nat_source', conf) + if conf.exists(['vpp', 'nat44', 'static']): + set_dependents('vpp_nat_static', conf) if not conf.exists(base): return { diff --git a/src/conf_mode/vpp_nat_static.py b/src/conf_mode/vpp_nat_static.py new file mode 100644 index 000000000..50384a00b --- /dev/null +++ b/src/conf_mode/vpp_nat_static.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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.configdiff import Diff +from vyos.configdiff import get_config_diff +from vyos.configdict import node_changed +from vyos.config import Config +from vyos import ConfigError +from vyos.vpp.nat.nat44 import Nat44Static + + +protocol_map = { + 'all': 0, + 'icmp': 1, + 'tcp': 6, + 'udp': 17, +} + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat44', 'static'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dictionary per interface delete + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + in_iface_add = [] + in_iface_del = [] + out_iface_add = [] + out_iface_del = [] + + changed_rules = node_changed( + conf, + base + ['rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + diff = get_config_diff(conf) + + for rule in changed_rules: + base_rule = base + ['rule', rule] + tmp = node_changed( + conf, + base_rule, + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + if 'inbound_interface' in tmp: + new, old = diff.get_value_diff(base_rule + ['inbound-interface']) + in_iface_add.append(new) if new else None + in_iface_del.append(old) if old else None + if 'outbound_interface' in tmp: + new, old = diff.get_value_diff(base_rule + ['outbound-interface']) + out_iface_add.append(new) if new else None + out_iface_del.append(old) if old else None + + final_in_iface_add = list(set(in_iface_add) - set(in_iface_del)) + final_in_iface_del = list(set(in_iface_del) - set(in_iface_add)) + final_out_iface_add = list(set(out_iface_add) - set(out_iface_del)) + final_out_iface_del = list(set(out_iface_del) - set(out_iface_add)) + + config.update( + { + 'in_iface_add': final_in_iface_add, + 'in_iface_del': final_in_iface_del, + 'out_iface_add': final_out_iface_add, + 'out_iface_del': final_out_iface_del, + 'changed_rules': changed_rules, + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + required_keys = {'inbound_interface', 'outbound_interface'} + for rule, rule_config in config['rule'].items(): + missing_keys = required_keys - rule_config.keys() + if missing_keys: + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')} in rule {rule}" + ) + + if not rule_config.get('translation', {}).get('address'): + raise ConfigError(f'Translation requires address in rule {rule}') + + has_dest_port = 'port' in rule_config.get('destination', {}) + has_trans_port = 'port' in rule_config.get('translation', {}) + + if not has_trans_port == has_dest_port: + raise ConfigError( + 'Source and destination ports must either both be specified, or neither must be specified' + ) + + +def generate(config): + pass + + +def apply(config): + n = Nat44Static() + + # Delete inbound interfaces + for interface in config['in_iface_del']: + n.delete_inbound_interface(interface) + # Delete outbound interfaces + for interface in config['out_iface_del']: + n.delete_outbound_interface(interface) + # Delete NAT static mapping rules + for rule in config['changed_rules']: + if rule in config.get('effective', {}).get('rule', {}): + rule_config = config['effective']['rule'][rule] + n.delete_nat44_static_mapping( + iface_out=rule_config.get('outbound_interface'), + local_ip=rule_config.get('translation').get('address'), + external_ip=rule_config.get('destination', {}).get('address', ''), + local_port=int(rule_config.get('translation', {}).get('port', 0)), + external_port=int(rule_config.get('destination', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + use_iface=( + True + if not rule_config.get('destination', {}).get('address') + else False + ), + ) + + if 'remove' in config: + return None + + # Add NAT44 static mapping rules + n.enable_nat44_ed() + for interface in config['in_iface_add']: + n.add_inbound_interface(interface) + for interface in config['out_iface_add']: + n.add_outbound_interface(interface) + for rule in config['changed_rules']: + if rule in config.get('rule', {}): + rule_config = config['rule'][rule] + n.add_nat44_static_mapping( + iface_out=rule_config.get('outbound_interface'), + local_ip=rule_config.get('translation').get('address'), + external_ip=rule_config.get('destination', {}).get('address', ''), + local_port=int(rule_config.get('translation', {}).get('port', 0)), + external_port=int(rule_config.get('destination', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + use_iface=( + True + if not rule_config.get('destination', {}).get('address') + else False + ), + ) + + +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_nat44.py b/src/op_mode/show_vpp_nat44.py new file mode 100644 index 000000000..1aae8164e --- /dev/null +++ b/src/op_mode/show_vpp_nat44.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 json +import sys +from tabulate import tabulate + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +protocol_map = { + 0: 'all', + 1: 'icmp', + 6: 'tcp', + 17: 'udp', +} + + +def _verify(func): + """Decorator checks if config for VPP NAT44 exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + base = 'vpp nat44' + if not config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured') + + return func(*args, **kwargs) + + return _wrapper + + +def _get_raw_output_sessions(vpp_api): + users: list[dict] = vpp_api.nat44_user_dump() + sessions_list: list[dict] = [] + for user in users: + ip_address = str(user._asdict().get('ip_address')) + user_sessions_dump = vpp_api.nat44_user_session_v3_dump(ip_address=ip_address) + user_sessions = [ + json.loads(json.dumps(session._asdict(), default=str)) + for session in user_sessions_dump + ] + sessions_list.extend(user_sessions) + return sorted(sessions_list, key=lambda x: x["inside_ip_address"]) + + +def _get_formatted_output_sessions(sessions_list): + print('NAT44 ED sessions:') + print(f'--------------- {len(sessions_list)} sessions ---------------') + for session in sessions_list: + in_ip_addr = session.get('inside_ip_address') + in_port = session.get('inside_port') + out_ip_addr = session.get('outside_ip_address') + out_port = session.get('outside_port') + protocol = protocol_map[session.get('protocol')].upper() + last_heard = session.get('last_heard') + time_since_last_heard = session.get('time_since_last_heard') + total_bytes = session.get('total_bytes') + total_pkts = session.get('total_pkts') + ext_host_address = session.get('ext_host_address') + ext_host_port = session.get('ext_host_port') + is_timed_out = session.get('is_timed_out') + + print(f' i2o {in_ip_addr} proto {protocol} port {in_port}') + print(f' o2i {out_ip_addr} proto {protocol} port {out_port}') + print(f' external host {ext_host_address}:{ext_host_port}') + print( + f' i2o flow: match: saddr {in_ip_addr} sport {in_port} daddr {ext_host_address} dport {ext_host_port} proto {protocol} rewrite: saddr {out_ip_addr}' + + ( + f' sport {out_port}' + if protocol != 'ICMP' + else f' daddr {ext_host_address} icmp-id {ext_host_port}' + ) + ) + print( + f' o2i flow: match: saddr {ext_host_address} sport {ext_host_port} daddr {out_ip_addr} dport {out_port} proto {protocol} rewrite: ' + + ( + f'daddr {in_ip_addr} dport {in_port}' + if protocol != 'ICMP' + else f' saddr {ext_host_address} daddr {in_ip_addr} icmp-id {ext_host_port}' + ) + ) + print(f' last heard {last_heard}') + print(f' time since last heard {time_since_last_heard}') + print(f' total packets {total_pkts}, total bytes {total_bytes}') + if is_timed_out: + print(' session timed out') + print('\n') + + +def _get_raw_output_static_rules(vpp_api): + nat_static_dump = vpp_api.nat44_static_mapping_dump() + rules_list = [ + json.loads(json.dumps(rule._asdict(), default=str)) for rule in nat_static_dump + ] + return rules_list + + +def _get_formatted_output_rules(vpp, rules_list): + data_entries = [] + for rule in rules_list: + dest_address = rule.get('external_ip_address') + dest_port = rule.get('external_port') or '' + trans_address = rule.get('local_ip_address') + trans_port = rule.get('local_port') or '' + protocol = protocol_map[rule.get('protocol', 0)] + dest_sh_if_index = rule.get('external_sw_if_index') + + vpp_if_name = vpp.get_interface_name(dest_sh_if_index) + if vpp_if_name: + dest_address = vpp_if_name + + values = [dest_address, dest_port, trans_address, trans_port, protocol] + data_entries.append(values) + headers = [ + 'Des_address/interface', + 'Dest_port', + 'Trans_address', + 'Trans_port', + 'Protocol', + ] + out = sorted(data_entries, key=lambda x: x[2]) + return tabulate(out, headers=headers, tablefmt='simple') + + +@_verify +def show_sessions(raw: bool): + vpp = VPPControl() + sessions_list: list[dict] = _get_raw_output_sessions(vpp.api) + + if raw: + return sessions_list + + else: + return _get_formatted_output_sessions(sessions_list) + + +@_verify +def show_summary(raw: bool): + vpp = VPPControl() + return vpp.cli_cmd('show nat44 summary').reply + + +@_verify +def show_static(raw: bool): + vpp = VPPControl() + rules_list: list[dict] = _get_raw_output_static_rules(vpp.api) + + if raw: + return rules_list + + else: + return _get_formatted_output_rules(vpp, rules_list) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 4d32bcddda72c1df944470f06b75908b2dc31667 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:15:42 +0300 Subject: T7315: Change CLI fot VPP NAT (#25) New CLI ``` set vpp nat44 static rule 10 outside-interface 'eth0' set vpp nat44 static rule 10 inside-interface 'eth1' set vpp nat44 static rule 10 external address 192.168.122.10 set vpp nat44 static rule 10 external port 6545 # optional set vpp nat44 static rule 10 protocol tcp|udp|icmp|all # optional, defaults to "all" set vpp nat44 static rule 10 local address 100.64.0.10 set vpp nat44 static rule 10 local port 64010 # optional ``` --- interface-definitions/vpp.xml.in | 25 ++++++++-------- python/vyos/vpp/nat/nat44.py | 32 ++++---------------- src/conf_mode/vpp_nat_source.py | 10 +++---- src/conf_mode/vpp_nat_static.py | 63 +++++++++++++++++----------------------- src/op_mode/show_vpp_nat44.py | 27 +++++++---------- 5 files changed, 61 insertions(+), 96 deletions(-) (limited to 'python') diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 66a38f546..263f9cb91 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -853,18 +853,18 @@ 320 - + - Inbound interface of NAT traffic + NAT inside interface any - + - Outbound interface of NAT traffic + NAT outside interface any @@ -920,7 +920,7 @@ - + NAT external parameters @@ -968,27 +968,25 @@ all - + - Outbound interface of NAT traffic + NAT outside interface - any - + - Inbound interface of NAT traffic + NAT inside interface - any - + - NAT internal parameters + NAT local parameters @@ -1006,6 +1004,7 @@ #include + #include diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index 985e5a583..97c4518e6 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -151,31 +151,24 @@ class Nat44Static(Nat44): def __init__(self): self.vpp = VPPControl() - def add_inbound_interface(self, interface_in): + def add_inside_interface(self, interface_in): self.interface_in = interface_in self.add_nat44_interface_inside() - def delete_inbound_interface(self, interface_in): + def delete_inside_interface(self, interface_in): self.interface_in = interface_in self.delete_nat44_interface_inside() - def add_outbound_interface(self, interface_out): + def add_outside_interface(self, interface_out): self.interface_out = interface_out self.add_nat44_interface_outside() - def delete_outbound_interface(self, interface_out): + def delete_outside_interface(self, interface_out): self.interface_out = interface_out self.delete_nat44_interface_outside() def add_nat44_static_mapping( - self, - iface_out, - local_ip, - external_ip, - local_port, - external_port, - protocol, - use_iface, + self, local_ip, external_ip, local_port, external_port, protocol ): """Add NAT44 static mapping""" self.vpp.api.nat44_add_del_static_mapping_v2( @@ -185,21 +178,11 @@ class Nat44Static(Nat44): local_port=local_port, external_port=external_port, flags=0x08 if not (protocol or local_port) else 0x00, - external_sw_if_index=( - self.vpp.get_sw_if_index(iface_out) if use_iface else 0xFFFFFFFF - ), is_add=True, ) def delete_nat44_static_mapping( - self, - iface_out, - local_ip, - external_ip, - local_port, - external_port, - protocol, - use_iface, + self, local_ip, external_ip, local_port, external_port, protocol ): """Delete NAT44 static mapping""" self.vpp.api.nat44_add_del_static_mapping_v2( @@ -209,8 +192,5 @@ class Nat44Static(Nat44): local_port=local_port, external_port=external_port, flags=0x08 if not (protocol or local_port) else 0x00, - external_sw_if_index=( - self.vpp.get_sw_if_index(iface_out) if use_iface else 0xFFFFFFFF - ), is_add=False, ) diff --git a/src/conf_mode/vpp_nat_source.py b/src/conf_mode/vpp_nat_source.py index 2e1884c3a..40b16a3c8 100644 --- a/src/conf_mode/vpp_nat_source.py +++ b/src/conf_mode/vpp_nat_source.py @@ -61,7 +61,7 @@ def verify(config): if 'remove' in config: return None - required_keys = {'inbound_interface', 'outbound_interface'} + required_keys = {'inside_interface', 'outside_interface'} if not all(key in config for key in required_keys): missing_keys = required_keys - set(config.keys()) raise ConfigError( @@ -83,8 +83,8 @@ def apply(config): # Delete NAT source if 'effective' in config: remove_config = config.get('effective') - interface_in = remove_config.get('inbound_interface') - interface_out = remove_config.get('outbound_interface') + interface_in = remove_config.get('inside_interface') + interface_out = remove_config.get('outside_interface') translation_address = remove_config.get('translation', {}).get('address') n = Nat44(interface_in, interface_out, translation_address) @@ -96,8 +96,8 @@ def apply(config): return None # Add NAT44 - interface_in = config.get('inbound_interface') - interface_out = config.get('outbound_interface') + interface_in = config.get('inside_interface') + interface_out = config.get('outside_interface') translation_address = config.get('translation', {}).get('address') n = Nat44(interface_in, interface_out, translation_address) diff --git a/src/conf_mode/vpp_nat_static.py b/src/conf_mode/vpp_nat_static.py index 50384a00b..b890ea150 100644 --- a/src/conf_mode/vpp_nat_static.py +++ b/src/conf_mode/vpp_nat_static.py @@ -86,12 +86,12 @@ def get_config(config=None) -> dict: expand_nodes=Diff.DELETE | Diff.ADD, ) - if 'inbound_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['inbound-interface']) + if 'inside_interface' in tmp: + new, old = diff.get_value_diff(base_rule + ['inside-interface']) in_iface_add.append(new) if new else None in_iface_del.append(old) if old else None - if 'outbound_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['outbound-interface']) + if 'outside_interface' in tmp: + new, old = diff.get_value_diff(base_rule + ['outside-interface']) out_iface_add.append(new) if new else None out_iface_del.append(old) if old else None @@ -120,7 +120,7 @@ def verify(config): if 'remove' in config: return None - required_keys = {'inbound_interface', 'outbound_interface'} + required_keys = {'inside_interface', 'outside_interface'} for rule, rule_config in config['rule'].items(): missing_keys = required_keys - rule_config.keys() if missing_keys: @@ -128,13 +128,16 @@ def verify(config): f"Required options are missing: {', '.join(missing_keys).replace('_', '-')} in rule {rule}" ) - if not rule_config.get('translation', {}).get('address'): - raise ConfigError(f'Translation requires address in rule {rule}') + if not rule_config.get('local', {}).get('address'): + raise ConfigError(f'Local settings require address in rule {rule}') - has_dest_port = 'port' in rule_config.get('destination', {}) - has_trans_port = 'port' in rule_config.get('translation', {}) + if not rule_config.get('external', {}).get('address'): + raise ConfigError(f'External settings require address in rule {rule}') - if not has_trans_port == has_dest_port: + has_local_port = 'port' in rule_config.get('local', {}) + has_external_port = 'port' in rule_config.get('external', {}) + + if not has_external_port == has_local_port: raise ConfigError( 'Source and destination ports must either both be specified, or neither must be specified' ) @@ -147,28 +150,22 @@ def generate(config): def apply(config): n = Nat44Static() - # Delete inbound interfaces + # Delete inside interfaces for interface in config['in_iface_del']: - n.delete_inbound_interface(interface) - # Delete outbound interfaces + n.delete_inside_interface(interface) + # Delete outside interfaces for interface in config['out_iface_del']: - n.delete_outbound_interface(interface) + n.delete_outside_interface(interface) # Delete NAT static mapping rules for rule in config['changed_rules']: if rule in config.get('effective', {}).get('rule', {}): rule_config = config['effective']['rule'][rule] n.delete_nat44_static_mapping( - iface_out=rule_config.get('outbound_interface'), - local_ip=rule_config.get('translation').get('address'), - external_ip=rule_config.get('destination', {}).get('address', ''), - local_port=int(rule_config.get('translation', {}).get('port', 0)), - external_port=int(rule_config.get('destination', {}).get('port', 0)), + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), protocol=protocol_map[rule_config.get('protocol', 'all')], - use_iface=( - True - if not rule_config.get('destination', {}).get('address') - else False - ), ) if 'remove' in config: @@ -177,24 +174,18 @@ def apply(config): # Add NAT44 static mapping rules n.enable_nat44_ed() for interface in config['in_iface_add']: - n.add_inbound_interface(interface) + n.add_inside_interface(interface) for interface in config['out_iface_add']: - n.add_outbound_interface(interface) + n.add_outside_interface(interface) for rule in config['changed_rules']: if rule in config.get('rule', {}): rule_config = config['rule'][rule] n.add_nat44_static_mapping( - iface_out=rule_config.get('outbound_interface'), - local_ip=rule_config.get('translation').get('address'), - external_ip=rule_config.get('destination', {}).get('address', ''), - local_port=int(rule_config.get('translation', {}).get('port', 0)), - external_port=int(rule_config.get('destination', {}).get('port', 0)), + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), protocol=protocol_map[rule_config.get('protocol', 'all')], - use_iface=( - True - if not rule_config.get('destination', {}).get('address') - else False - ), ) diff --git a/src/op_mode/show_vpp_nat44.py b/src/op_mode/show_vpp_nat44.py index 1aae8164e..fcf28f76a 100644 --- a/src/op_mode/show_vpp_nat44.py +++ b/src/op_mode/show_vpp_nat44.py @@ -116,27 +116,22 @@ def _get_raw_output_static_rules(vpp_api): return rules_list -def _get_formatted_output_rules(vpp, rules_list): +def _get_formatted_output_rules(rules_list): data_entries = [] for rule in rules_list: - dest_address = rule.get('external_ip_address') - dest_port = rule.get('external_port') or '' - trans_address = rule.get('local_ip_address') - trans_port = rule.get('local_port') or '' + external_address = rule.get('external_ip_address') + external_port = rule.get('external_port') or '' + local_address = rule.get('local_ip_address') + local_port = rule.get('local_port') or '' protocol = protocol_map[rule.get('protocol', 0)] - dest_sh_if_index = rule.get('external_sw_if_index') - vpp_if_name = vpp.get_interface_name(dest_sh_if_index) - if vpp_if_name: - dest_address = vpp_if_name - - values = [dest_address, dest_port, trans_address, trans_port, protocol] + values = [external_address, external_port, local_address, local_port, protocol] data_entries.append(values) headers = [ - 'Des_address/interface', - 'Dest_port', - 'Trans_address', - 'Trans_port', + 'External address', + 'External port', + 'Local address', + 'Local port', 'Protocol', ] out = sorted(data_entries, key=lambda x: x[2]) @@ -170,7 +165,7 @@ def show_static(raw: bool): return rules_list else: - return _get_formatted_output_rules(vpp, rules_list) + return _get_formatted_output_rules(rules_list) if __name__ == '__main__': -- cgit v1.2.3 From fa293316e679645e6397697428cee96e32e346fc Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Wed, 23 Apr 2025 15:11:16 +0300 Subject: T7181: VPP Static and dynamic NAT New CLI for static and dynamic NAT: ``` set vpp nat44 interface outside # multi set vpp nat44 interface inside # multi set vpp nat44 address-pool translation interface # multi set vpp nat44 address-pool translation address
# multi set vpp nat44 address-pool twice-nat interface # multi set vpp nat44 address-pool twice-nat address
# multi set vpp nat44 static rule external address
set vpp nat44 static rule external port set vpp nat44 static rule local address
set vpp nat44 static rule local port set vpp nat44 static rule protocol set vpp nat44 static rule options twice-nat set vpp nat44 static rule options self-twice-nat set vpp nat44 static rule options out-to-in-only set vpp nat44 static rule options twice-nat-address
set vpp nat44 exclude rule protocol set vpp nat44 exclude rule local-port set vpp nat44 exclude rule local-address
set vpp nat44 exclude rule external-interface ``` Settings: ``` set vpp settings nat44 session-limit # default 64512 set vpp settings nat44 timeout udp # default 300 set vpp settings nat44 timeout tcp-established # default 7440 set vpp settings nat44 timeout tcp-transitory # default 240 set vpp settings nat44 timeout icmp # default 60 set vpp settings nat44 workers set vpp settings nat44 no-forwarding ``` --- data/config-mode-dependencies/vyos-vpp.json | 17 +- .../include/vpp/nat_address_range.xml.i | 20 + .../include/vpp/nat_interface.xml.i | 11 + .../include/vpp/nat_protocol.xml.i | 30 ++ interface-definitions/vpp.xml.in | 293 +++++++++---- op-mode-definitions/show_vpp_nat44.xml.in | 12 + python/vyos/vpp/control_vpp.py | 50 ++- python/vyos/vpp/nat/nat44.py | 210 +++++----- src/conf_mode/vpp.py | 48 ++- src/conf_mode/vpp_nat.py | 458 +++++++++++++++++++++ src/conf_mode/vpp_nat_source.py | 119 ------ src/conf_mode/vpp_nat_static.py | 200 --------- src/op_mode/show_vpp_nat44.py | 91 +++- 13 files changed, 1043 insertions(+), 516 deletions(-) create mode 100644 interface-definitions/include/vpp/nat_address_range.xml.i create mode 100644 interface-definitions/include/vpp/nat_interface.xml.i create mode 100644 interface-definitions/include/vpp/nat_protocol.xml.i create mode 100644 src/conf_mode/vpp_nat.py delete mode 100644 src/conf_mode/vpp_nat_source.py delete mode 100644 src/conf_mode/vpp_nat_static.py (limited to 'python') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 307e763b9..46c1ee9f2 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -10,43 +10,42 @@ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"], "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], - "vpp_nat_static": ["vpp_nat_static"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ethernet": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_geneve": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_gre": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ipip": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_loopback": { - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_vxlan": { "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] } } diff --git a/interface-definitions/include/vpp/nat_address_range.xml.i b/interface-definitions/include/vpp/nat_address_range.xml.i new file mode 100644 index 000000000..48648f91a --- /dev/null +++ b/interface-definitions/include/vpp/nat_address_range.xml.i @@ -0,0 +1,20 @@ + + + + IP address or range + + ipv4 + IPv4 address + + + ipv4range + IPv4 address range + + + + + + + + + diff --git a/interface-definitions/include/vpp/nat_interface.xml.i b/interface-definitions/include/vpp/nat_interface.xml.i new file mode 100644 index 000000000..20a7356bf --- /dev/null +++ b/interface-definitions/include/vpp/nat_interface.xml.i @@ -0,0 +1,11 @@ + + + + Add IP address from an interface + + + + + + + diff --git a/interface-definitions/include/vpp/nat_protocol.xml.i b/interface-definitions/include/vpp/nat_protocol.xml.i new file mode 100644 index 000000000..b88fd2b62 --- /dev/null +++ b/interface-definitions/include/vpp/nat_protocol.xml.i @@ -0,0 +1,30 @@ + + + + Protocol + + tcp udp icmp all + + + all + All protocols (TCP, UDP, and ICMP) + + + icmp + Internet Control Message Protocol (ICMP) + + + tcp + Transmission Control Protocol (TCP) + + + udp + User Datagram Protocol (UDP) + + + (tcp|udp|icmp|all) + + + all + + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 263f9cb91..b1ae348da 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -788,6 +788,98 @@ + + + NAT settings + + + + + NAT44 session timeouts + + + + + ICMP timeout + + u32 + Timeout in seconds (default: 60) + + + 60 + + + + TCP established timeout + + u32 + TCP established timeout in seconds (default: 7440) + + + 7440 + + + + TCP transitory timeout + + u32 + Timeout in seconds (default: 240) + + + 240 + + + + UDP timeout + + u32 + Timeout in seconds (default: 60) + + + 60 + + + + + + Maximum number of sessions per thread + + u32 + Number of sessions + + + + + Number of sessions must be between 1 and 4294967295 + + 64512 + + + + List of NAT workers + + <id> + Worker id + + + <idN>-<idM> + Worker id range (use '-' as delimiter) + + + + + Not a valid value or range + + + + + + Do not forward packets which do not match existing NAT translations (static or dynamic) + + + + + Physical memory settings @@ -842,73 +934,65 @@ - + NAT44 + 320 - + - Source NAT setting - 320 + NAT interface setting - + NAT inside interface - any + - + NAT outside interface - any + + + + + + NAT address pool + + - Outside NAT IP (source NAT only) + NAT translation pool - - - IP address, subnet, or range - - masquerade - - - ipv4 - IPv4 address to match - - - ipv4range - IPv4 address range to match - - - masquerade - NAT to the primary address of outbound-interface - - - - - (masquerade) - - - + #include + #include + + + + + NAT twice-nat pool + + + #include + #include - + - Static NAT settings - 321 + Static NAT rules @@ -920,6 +1004,7 @@ + #include NAT external parameters @@ -940,50 +1025,6 @@ #include - - - Protocol to NAT - - tcp udp icmp all - - - all - All protocols (TCP, UDP, and ICMP) - - - icmp - Internet control message protocol - - - tcp - Transmission control protocol - - - udp - user datagram protocol - - - (tcp|udp|icmp|all) - - - all - - - - NAT outside interface - - - - - - - - NAT inside interface - - - - - NAT local parameters @@ -1004,6 +1045,96 @@ #include + + + NAT static mapping options + + + + + Rewrite source IP addresses on packets sent from outside to inside + + + + + + Rewrite source IP addresses on packets sent only from a local address to an external address + + + + + + Only apply rule for traffic from outside to inside interfaces + + + + + + Force use of specific IP address from twice-nat address pool + + ipv4 + IPv4 address + + + + + + + + + #include + + + + + + + Exclude packets matching these rules from NAT + + + + + Rule number + + u32 + Number of rule + + + + + + IP address of the internal (local) device + + ipv4 + IPv4 address + + + + + + + + + Port number used by connection on internal device + + u32:1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + #include + + + External interface + + + + + #include diff --git a/op-mode-definitions/show_vpp_nat44.xml.in b/op-mode-definitions/show_vpp_nat44.xml.in index f2e77267a..96d53fba9 100644 --- a/op-mode-definitions/show_vpp_nat44.xml.in +++ b/op-mode-definitions/show_vpp_nat44.xml.in @@ -27,6 +27,18 @@ sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_summary + + + Show VPP NAT44 pool addresses + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_addresses + + + + Show VPP NAT44 interfaces + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_interfaces + diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 9d45bbd91..5db0ea560 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 2023-2025 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 @@ -439,6 +439,54 @@ class VPPControl: return iface.interface_dev_type return None + @_Decorators.api_call + def enable_disable_nat44_forwarding(self, enable: bool) -> None: + """Enable/disable NAT44 forwarding + + Args: + enable (bool): True if enable, False if disable + """ + self.__vpp_api_client.api.nat44_forwarding_enable_disable(enable=enable) + + @_Decorators.api_call + def set_nat_timeouts( + self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int + ) -> None: + """Set NAT timeouts + + Args: + tcp_established (int): TCP established timeout + tcp_transitory (int): TCP transitory timeout + udp (int): UDP timeout + icmp (int): ICMP timeout + """ + self.__vpp_api_client.api.nat_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) + + @_Decorators.api_call + def set_nat44_session_limit(self, session_limit: int) -> None: + """Set NAT44 session limit + + Args: + session_limit (int): Maximum number of sessions per thread + """ + self.__vpp_api_client.api.nat44_set_session_limit( + session_limit=session_limit, + ) + + @_Decorators.api_call + def set_nat_workers(self, workers: int) -> None: + """Set NAT44 session limit + + Args: + workers (int): Bitmask of workers list + """ + self.__vpp_api_client.api.nat_set_workers(worker_mask=workers) + @property def connected(self) -> bool: """Check if VPP API is connected diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index 97c4518e6..e01e05153 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -18,16 +18,20 @@ from vyos.vpp import VPPControl +# NAT44 flags +NAT_IS_NONE = 0x00 +NAT_IS_TWICE_NAT = 0x01 +NAT_IS_SELF_TWICE_NAT = 0x02 +NAT_IS_OUT2IN_ONLY = 0x04 +NAT_IS_ADDR_ONLY = 0x08 +NAT_IS_OUTSIDE = 0x10 +NAT_IS_INSIDE = 0x20 + +NO_INTERFACE = 0xFFFFFFFF + + class Nat44: - def __init__( - self, - interface_in: str, - interface_out: str, - translation_pool: str, - ): - self.interface_in = interface_in - self.interface_out = interface_out - self.translation_pool = translation_pool + def __init__(self): self.vpp = VPPControl() def enable_nat44_ed(self): @@ -40,6 +44,10 @@ class Nat44: """ self.vpp.api.nat44_ed_plugin_enable_disable(enable=True) + def disable_nat44_ed(self): + """Disable NAT44 endpoint dependent plugin""" + self.vpp.api.nat44_ed_plugin_enable_disable(enable=False) + def enable_nat44_ei(self): """Enable NAT44 endpoint independent plugin Example: @@ -49,148 +57,164 @@ class Nat44: """ self.vpp.api.nat44_ei_plugin_enable_disable(enable=True) - def enable_nat44_forwarding(self): - """Enable NAT44 forwarding - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44() - nat44.enable_nat44_forwarding() - """ - self.vpp.api.nat44_forwarding_enable_disable(enable=True) - - def disable_nat44_forwarding(self): - """Disable NAT44 forwarding - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44() - nat44.disable_nat44_forwarding() - """ - self.vpp.api.nat44_forwarding_enable_disable(enable=False) - - def add_nat44_out_interface(self): - """Add NAT44 output interface - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44('eth0') - nat44.add_nat44_out_interface() - """ - self.vpp.api.nat44_ed_add_del_output_interface( - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), - is_add=True, - ) - - def delete_nat44_out_interface(self): - """Delete NAT44 output interface""" - self.vpp.api.nat44_ed_add_del_output_interface( - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), - is_add=False, - ) - - def add_nat44_interface_inside(self): + def add_nat44_interface_inside(self, interface_in): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x20, - sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), is_add=True, ) - def delete_nat44_interface_inside(self): + def delete_nat44_interface_inside(self, interface_in): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x20, - sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), is_add=False, ) - def add_nat44_interface_outside(self): + def add_nat44_interface_outside(self, interface_out): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x10, - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), is_add=True, ) - def delete_nat44_interface_outside(self): + def delete_nat44_interface_outside(self, interface_out): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x10, - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), is_add=False, ) - def add_nat44_address_range(self): + def add_nat44_address_range(self, addresses, twice_nat): """Add NAT44 address range""" - if '-' not in self.translation_pool and self.translation_pool != 'masquerade': - first_ip_address = last_ip_address = self.translation_pool + if '-' not in addresses: + first_ip_address = last_ip_address = addresses else: - first_ip_address, last_ip_address = self.translation_pool.split('-') + first_ip_address, last_ip_address = addresses.split('-') self.vpp.api.nat44_add_del_address_range( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, first_ip_address=first_ip_address, last_ip_address=last_ip_address, is_add=True, ) - def delete_nat44_address_range(self): + def delete_nat44_address_range(self, addresses, twice_nat): """Delete NAT44 address range""" - if '-' not in self.translation_pool and self.translation_pool != 'masquerade': - first_ip_address = last_ip_address = self.translation_pool + if '-' not in addresses: + first_ip_address = last_ip_address = addresses else: - first_ip_address, last_ip_address = self.translation_pool.split('-') + first_ip_address, last_ip_address = addresses.split('-') self.vpp.api.nat44_add_del_address_range( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, first_ip_address=first_ip_address, last_ip_address=last_ip_address, is_add=False, ) - def enable_ipfix(self): - """Enable NAT44 IPFIX logging""" - self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) - - -class Nat44Static(Nat44): - def __init__(self): - self.vpp = VPPControl() - - def add_inside_interface(self, interface_in): - self.interface_in = interface_in - self.add_nat44_interface_inside() - - def delete_inside_interface(self, interface_in): - self.interface_in = interface_in - self.delete_nat44_interface_inside() - - def add_outside_interface(self, interface_out): - self.interface_out = interface_out - self.add_nat44_interface_outside() + def add_nat44_interface_address(self, interface, twice_nat): + """Add NAT44 interface address""" + self.vpp.api.nat44_add_del_interface_addr( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=True, + ) - def delete_outside_interface(self, interface_out): - self.interface_out = interface_out - self.delete_nat44_interface_outside() + def delete_nat44_interface_address(self, interface, twice_nat): + """Delete NAT44 interface address""" + self.vpp.api.nat44_add_del_interface_addr( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=False, + ) def add_nat44_static_mapping( - self, local_ip, external_ip, local_port, external_port, protocol + self, + local_ip, + external_ip, + local_port, + external_port, + protocol, + twice_nat, + self_twice_nat, + out2in, + pool_ip, ): """Add NAT44 static mapping""" + flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE + flags |= NAT_IS_TWICE_NAT if twice_nat else 0 + flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0 + flags |= NAT_IS_OUT2IN_ONLY if out2in else 0 self.vpp.api.nat44_add_del_static_mapping_v2( local_ip_address=local_ip, external_ip_address=external_ip, protocol=protocol, local_port=local_port, external_port=external_port, - flags=0x08 if not (protocol or local_port) else 0x00, + match_pool=True if pool_ip else False, + pool_ip_address=pool_ip if pool_ip else '', + flags=flags, is_add=True, ) def delete_nat44_static_mapping( - self, local_ip, external_ip, local_port, external_port, protocol + self, + local_ip, + external_ip, + local_port, + external_port, + protocol, + twice_nat, + self_twice_nat, + out2in, + pool_ip, ): """Delete NAT44 static mapping""" + flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE + flags |= NAT_IS_TWICE_NAT if twice_nat else 0 + flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0 + flags |= NAT_IS_OUT2IN_ONLY if out2in else 0 self.vpp.api.nat44_add_del_static_mapping_v2( local_ip_address=local_ip, external_ip_address=external_ip, protocol=protocol, local_port=local_port, external_port=external_port, - flags=0x08 if not (protocol or local_port) else 0x00, + match_pool=True if pool_ip else False, + pool_ip_address=pool_ip if pool_ip else '', + flags=flags, is_add=False, ) + + def add_nat44_identity_mapping(self, ip_address, protocol, port, interface): + """Add NAT44 identity mapping""" + self.vpp.api.nat44_add_del_identity_mapping( + ip_address=ip_address, + protocol=protocol, + port=port, + sw_if_index=( + self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE + ), + flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE, + is_add=True, + ) + + def delete_nat44_identity_mapping(self, ip_address, protocol, port, interface): + """Delete NAT44 identity mapping""" + self.vpp.api.nat44_add_del_identity_mapping( + ip_address=ip_address, + protocol=protocol, + port=port, + sw_if_index=( + self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE + ), + flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE, + is_add=False, + ) + + def enable_ipfix(self): + """Enable NAT44 IPFIX logging""" + self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index ca7d19160..7f9adc68b 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -136,10 +136,8 @@ def get_config(config=None): set_dependents('ethernet', conf, removed_iface) # NAT dependency - if conf.exists(['vpp', 'nat44', 'source']): - set_dependents('vpp_nat_source', conf) - if conf.exists(['vpp', 'nat44', 'static']): - set_dependents('vpp_nat_static', conf) + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) if not conf.exists(base): return { @@ -424,6 +422,7 @@ def verify(config): 'Only one multipoint GRE tunnel is allowed from the same source address' ) + workers = 0 if 'cpu' in config['settings']: if ( 'corelist_workers' in config['settings']['cpu'] @@ -496,6 +495,22 @@ def verify(config): if not all(el in cpus_available for el in all_core_numbers): raise ConfigError('"cpu corelist-workers" is not correct') + workers = len(all_core_numbers) + + if 'workers' in config['settings']['nat44']: + nat_workers = [] + for worker_range in config['settings']['nat44']['workers']: + worker_numbers = worker_range.split('-') + if int(worker_numbers[0]) > int(worker_numbers[-1]): + raise ConfigError( + f'Range for "nat44 workers {worker_range}" is not correct' + ) + nat_workers.extend( + range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1) + ) + if not all(el in list(range(workers)) for el in nat_workers): + raise ConfigError('"nat44 workers" is not correct') + verify_memory(config['settings']) if 'host_resources' in config['settings']: if ( @@ -716,6 +731,31 @@ def apply(config): # Syncronize routes via LCP vpp_control.lcp_resync() + # NAT44 settings + nat44_settings = config['settings'].get('nat44', {}) + + enable_forwarding = True + if 'no_forwarding' in nat44_settings: + enable_forwarding = False + vpp_control.enable_disable_nat44_forwarding(enable_forwarding) + + vpp_control.set_nat_timeouts( + icmp=int(nat44_settings.get('timeout').get('icmp')), + udp=int(nat44_settings.get('timeout').get('udp')), + tcp_established=int(nat44_settings.get('timeout').get('tcp_established')), + tcp_transitory=int(nat44_settings.get('timeout').get('tcp_transitory')), + ) + + vpp_control.set_nat44_session_limit(int(nat44_settings.get('session_limit'))) + + if nat44_settings.get('workers'): + bitmask = 0 + for worker_range in nat44_settings['workers']: + worker_numbers = worker_range.split('-') + for wid in range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1): + bitmask |= 1 << wid + vpp_control.set_nat_workers(bitmask) + # Save persistent config if 'persist_config' in config and config['persist_config']: persist_config.write('eth_ifaces', config['persist_config']) diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py new file mode 100644 index 000000000..d38599388 --- /dev/null +++ b/src/conf_mode/vpp_nat.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 ipaddress + +from vyos import ConfigError + +from vyos.configdiff import Diff +from vyos.configdict import node_changed +from vyos.config import Config +from vyos.utils.network import get_interface_address + +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp.nat.nat44 import Nat44 + + +protocol_map = { + 'all': 0, + 'icmp': 1, + 'tcp': 6, + 'udp': 17, +} + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat44'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + return config + + config_changed = node_changed( + conf, + base, + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_static_rules = node_changed( + conf, + base + ['static', 'rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_exclude_rules = node_changed( + conf, + base + ['exclude', 'rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + if not config_changed: + changed_static_rules = list(config.get('static', {}).get('rule', {}).keys()) + changed_exclude_rules = list(config.get('exclude', {}).get('rule', {}).keys()) + + config.update( + { + 'changed_static_rules': changed_static_rules, + 'changed_exclude_rules': changed_exclude_rules, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def convert_range_to_list_ips(address_range) -> list: + """Converts IP range to a list of IPs . + + Example: + % ip = IPOperations('192.0.0.1-192.0.2.5') + % ip.convert_prefix_to_list_ips() + ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5'] + """ + if '-' in address_range: + start_ip, end_ip = address_range.split('-') + start_ip = ipaddress.ip_address(start_ip) + end_ip = ipaddress.ip_address(end_ip) + return [ + str(ipaddress.ip_address(ip)) + for ip in range(int(start_ip), int(end_ip) + 1) + ] + else: + return [address_range] + + +def verify(config): + if 'remove' in config: + return None + + if 'interface' not in config: + raise ConfigError('Interfaces must be configured for NAT44') + + required_keys = {'inside', 'outside'} + missing_keys = required_keys - set(config['interface'].keys()) + if missing_keys: + raise ConfigError( + f'Both inside and outside interfaces must be configured. Please add: {", ".join(missing_keys)}' + ) + + for interface in config['interface']['inside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for inside NAT interface' + ) + for interface in config['interface']['outside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for outside NAT interface' + ) + + if not config.get('address_pool', {}).get('translation') and not config.get( + 'static', {} + ).get('rule'): + raise ConfigError('"address-pool translation" or "static rule" is required') + + addresses_translation = [] + addresses_twice_nat = [] + if 'address_pool' in config: + address_pool = config.get('address_pool') + if 'translation' in address_pool: + if not address_pool['translation'].get('address') and not address_pool[ + 'translation' + ].get('interface'): + raise ConfigError( + '"address-pool translation" requires address or interface' + ) + + for address_range in address_pool['translation'].get('address', []): + addresses = convert_range_to_list_ips(address_range) + for address in addresses: + if address in addresses_translation: + raise ConfigError( + f'Address {address} is already in use in "address-pool translation address"' + ) + addresses_translation.append(address) + + for interface in address_pool['translation'].get('interface', []): + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for "address-pool translation interface"' + ) + iface_address = get_interface_address(interface).get('addr_info', [])[0].get('local') + addresses_translation.append(iface_address) + + if 'twice_nat' in address_pool: + if not address_pool['twice_nat'].get('address') and not address_pool[ + 'twice_nat' + ].get('interface'): + raise ConfigError( + '"address-pool twice-nat" requires address or interface' + ) + + for address_range in address_pool['twice_nat'].get('address', []): + addresses = convert_range_to_list_ips(address_range) + for address in addresses: + if address in addresses_twice_nat: + raise ConfigError( + f'Address {address} is already in use in "address-pool twice-nat address"' + ) + addresses_twice_nat.append(address) + + for interface in address_pool['twice_nat'].get('interface', []): + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for "address-pool twice-nat interface"' + ) + iface_address = get_interface_address(interface).get('addr_info', [])[0].get('local') + addresses_twice_nat.append(iface_address) + + if 'static' in config: + addresses_with_ports = set() + addresses_without_ports = set() + local_addresses = set() + + for rule, rule_config in config['static']['rule'].items(): + error_msg = f'Configuration error in static rule {rule}:' + + if not rule_config.get('local', {}).get('address'): + raise ConfigError(f'{error_msg} local settings require address') + + if not rule_config.get('external', {}).get('address'): + raise ConfigError(f'{error_msg} external settings require address') + + has_local_port = 'port' in rule_config.get('local', {}) + has_external_port = 'port' in rule_config.get('external', {}) + + if not has_external_port == has_local_port: + raise ConfigError( + f'{error_msg} source and destination ports must either ' + 'both be specified, or neither must be specified' + ) + + ext_address = rule_config['external']['address'] + port = rule_config['external'].get('port') + local_address = rule_config['local']['address'] + + if port: + pair = (ext_address, port) + if ( + pair in addresses_with_ports + or ext_address in addresses_without_ports + ): + raise ConfigError( + f'{error_msg} external address/port is already in use!' + ) + addresses_with_ports.add(pair) + if ext_address not in addresses_translation: + raise ConfigError( + f'{error_msg} external address {ext_address} is not in "address-pool translation"' + ) + + else: + if ext_address in addresses_without_ports or any( + addr == ext_address for addr, _ in addresses_with_ports + ): + raise ConfigError( + f'{error_msg} external address is already in use!' + ) + addresses_without_ports.add(ext_address) + + if local_address in local_addresses: + raise ConfigError( + f'{error_msg} local address {local_address} is already in use' + ) + local_addresses.add(local_address) + + options = rule_config.get('options', {}) + if all(key in options for key in ('twice_nat', 'self_twice_nat')): + raise ConfigError( + f'{error_msg} cannot set both options "twice-nat" and "self-twice-nat"' + ) + if any(key in options for key in ('twice_nat', 'self_twice_nat')): + if not has_local_port or rule_config['protocol'] == 'all': + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat options require port and protocol to be set' + ) + if not config.get('address_pool', {}).get('twice_nat'): + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat options require "address-pool twice-nat" to be set' + ) + if 'twice_nat_address' in options: + if not any(key in options for key in ('twice_nat', 'self_twice_nat')): + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat option required when twice-nat-address is set' + ) + tn_address = options['twice_nat_address'] + if tn_address not in addresses_twice_nat: + raise ConfigError( + f'{error_msg} twice-nat-address {tn_address} is not in "address-pool twice-nat"' + ) + + if 'exclude' in config: + for rule, rule_config in config['exclude']['rule'].items(): + keys = {'local_address', 'external_interface'} + if not any(key in rule_config for key in keys): + raise ConfigError( + f'Local-address or external-interface must be specified for exclude rule {rule}' + ) + if all(key in rule_config for key in keys): + raise ConfigError( + f'Cannot set both address and interface for exclude rule {rule}' + ) + if ( + 'external_interface' in rule_config + and rule_config.get('external_interface') not in config['vpp_ifaces'] + ): + raise ConfigError( + f'{rule_config["external_interface"]} must be a VPP interface for exclude rule {rule}' + ) + + +def generate(config): + pass + + +def apply(config): + n = Nat44() + + if 'remove' in config: + n.disable_nat44_ed() + return None + + if 'effective' in config: + remove_config = config.get('effective') + # Delete inside interfaces + for interface in remove_config['interface']['inside']: + if interface not in config.get('interface', {}).get('inside', []): + n.delete_nat44_interface_inside(interface) + # Delete outside interfaces + for interface in remove_config['interface']['outside']: + if interface not in config.get('interface', {}).get('outside', []): + n.delete_nat44_interface_outside(interface) + # Delete address pool + address_pool = config.get('address_pool', {}) + for address in ( + remove_config.get('address_pool', {}) + .get('translation', {}) + .get('address', []) + ): + if address not in address_pool.get('translation', {}).get('address', []): + n.delete_nat44_address_range(address, twice_nat=False) + for interface in ( + remove_config.get('address_pool', {}) + .get('translation', {}) + .get('interface', []) + ): + if interface not in address_pool.get('translation', {}).get( + 'interface', [] + ): + n.delete_nat44_interface_address(interface, twice_nat=False) + for address in ( + remove_config.get('address_pool', {}) + .get('twice_nat', {}) + .get('address', []) + ): + if address not in address_pool.get('twice_nat', {}).get('address', []): + n.delete_nat44_address_range(address, twice_nat=True) + for interface in ( + remove_config.get('address_pool', {}) + .get('twice_nat', {}) + .get('interface', []) + ): + if interface not in address_pool.get('twice_nat', {}).get('interface', []): + n.delete_nat44_interface_address(interface, twice_nat=True) + # Delete NAT static mapping rules + for rule in config['changed_static_rules']: + if rule in remove_config.get('static', {}).get('rule', {}): + rule_config = remove_config['static']['rule'][rule] + n.delete_nat44_static_mapping( + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + twice_nat='twice_nat' in rule_config.get('options', {}), + self_twice_nat='self_twice_nat' in rule_config.get('options', {}), + out2in='out_to_in_only' in rule_config.get('options', {}), + pool_ip=rule_config.get('options', {}).get('twice_nat_address'), + ) + # Delete NAT exclude rules + for rule in config['changed_exclude_rules']: + if rule in remove_config.get('exclude', {}).get('rule', {}): + rule_config = remove_config['exclude']['rule'][rule] + n.delete_nat44_identity_mapping( + ip_address=rule_config.get('local_address'), + protocol=protocol_map[rule_config.get('protocol', 'all')], + port=int(rule_config.get('local_port', 0)), + interface=rule_config.get('external_interface'), + ) + + # Add NAT44 + n.enable_nat44_ed() + # Add inside interfaces + for interface in config['interface']['inside']: + n.add_nat44_interface_inside(interface) + # Add outside interfaces + for interface in config['interface']['outside']: + n.add_nat44_interface_outside(interface) + # Add translation pool + for address in ( + config.get('address_pool', {}).get('translation', {}).get('address', []) + ): + n.add_nat44_address_range(address, twice_nat=False) + for interface in ( + config.get('address_pool', {}).get('translation', {}).get('interface', []) + ): + n.add_nat44_interface_address(interface, twice_nat=False) + for address in ( + config.get('address_pool', {}).get('twice_nat', {}).get('address', []) + ): + n.add_nat44_address_range(address, twice_nat=True) + for interface in ( + config.get('address_pool', {}).get('twice_nat', {}).get('interface', []) + ): + n.add_nat44_interface_address(interface, twice_nat=True) + # Add NAT static mapping rules + for rule in config['changed_static_rules']: + if rule in config.get('static', {}).get('rule', {}): + rule_config = config['static']['rule'][rule] + n.add_nat44_static_mapping( + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + twice_nat='twice_nat' in rule_config.get('options', {}), + self_twice_nat='self_twice_nat' in rule_config.get('options', {}), + out2in='out_to_in_only' in rule_config.get('options', {}), + pool_ip=rule_config.get('options', {}).get('twice_nat_address'), + ) + # Add NAT exclude rules + for rule in config['changed_exclude_rules']: + if rule in config.get('exclude', {}).get('rule', {}): + rule_config = config['exclude']['rule'][rule] + n.add_nat44_identity_mapping( + ip_address=rule_config.get('local_address'), + protocol=protocol_map[rule_config.get('protocol', 'all')], + port=int(rule_config.get('local_port', 0)), + interface=rule_config.get('external_interface'), + ) + + +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_nat_source.py b/src/conf_mode/vpp_nat_source.py deleted file mode 100644 index 40b16a3c8..000000000 --- a/src/conf_mode/vpp_nat_source.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2025 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 -from vyos import ConfigError -from vyos.vpp.nat.nat44 import Nat44 - - -def get_config(config=None) -> dict: - if config: - conf = config - else: - conf = Config() - - base = ['vpp', 'nat44', 'source'] - - # Get config_dict with default values - config = conf.get_config_dict( - base, - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_defaults=True, - with_recursive_defaults=True, - ) - - # Get effective config as we need full dicitonary per interface delete - effective_config = conf.get_config_dict( - base, - 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}) - - return config - - -def verify(config): - if 'remove' in config: - return None - - required_keys = {'inside_interface', 'outside_interface'} - 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('_', '-')}" - ) - - if not config.get('translation', {}).get('address'): - raise ConfigError('Translation requires address') - - if config.get('translation', {}).get('address') == 'masquerade': - raise ConfigError('Masquerade is not implemented') - - -def generate(config): - pass - - -def apply(config): - # Delete NAT source - if 'effective' in config: - remove_config = config.get('effective') - interface_in = remove_config.get('inside_interface') - interface_out = remove_config.get('outside_interface') - translation_address = remove_config.get('translation', {}).get('address') - - n = Nat44(interface_in, interface_out, translation_address) - n.delete_nat44_out_interface() - n.delete_nat44_interface_inside() - n.delete_nat44_address_range() - - if 'remove' in config: - return None - - # Add NAT44 - interface_in = config.get('inside_interface') - interface_out = config.get('outside_interface') - translation_address = config.get('translation', {}).get('address') - - n = Nat44(interface_in, interface_out, translation_address) - n.enable_nat44_ed() - n.enable_nat44_forwarding() - n.add_nat44_out_interface() - # n.add_nat44_interface_inside() - n.add_nat44_address_range() - - -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_nat_static.py b/src/conf_mode/vpp_nat_static.py deleted file mode 100644 index b890ea150..000000000 --- a/src/conf_mode/vpp_nat_static.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2025 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.configdiff import Diff -from vyos.configdiff import get_config_diff -from vyos.configdict import node_changed -from vyos.config import Config -from vyos import ConfigError -from vyos.vpp.nat.nat44 import Nat44Static - - -protocol_map = { - 'all': 0, - 'icmp': 1, - 'tcp': 6, - 'udp': 17, -} - - -def get_config(config=None) -> dict: - if config: - conf = config - else: - conf = Config() - - base = ['vpp', 'nat44', 'static'] - - # Get config_dict with default values - config = conf.get_config_dict( - base, - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_defaults=True, - with_recursive_defaults=True, - ) - - # Get effective config as we need full dictionary per interface delete - effective_config = conf.get_config_dict( - base, - key_mangling=('-', '_'), - effective=True, - get_first_key=True, - no_tag_node_value_mangle=True, - ) - - if not config: - config['remove'] = True - - in_iface_add = [] - in_iface_del = [] - out_iface_add = [] - out_iface_del = [] - - changed_rules = node_changed( - conf, - base + ['rule'], - key_mangling=('-', '_'), - recursive=True, - expand_nodes=Diff.DELETE | Diff.ADD, - ) - diff = get_config_diff(conf) - - for rule in changed_rules: - base_rule = base + ['rule', rule] - tmp = node_changed( - conf, - base_rule, - key_mangling=('-', '_'), - recursive=True, - expand_nodes=Diff.DELETE | Diff.ADD, - ) - - if 'inside_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['inside-interface']) - in_iface_add.append(new) if new else None - in_iface_del.append(old) if old else None - if 'outside_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['outside-interface']) - out_iface_add.append(new) if new else None - out_iface_del.append(old) if old else None - - final_in_iface_add = list(set(in_iface_add) - set(in_iface_del)) - final_in_iface_del = list(set(in_iface_del) - set(in_iface_add)) - final_out_iface_add = list(set(out_iface_add) - set(out_iface_del)) - final_out_iface_del = list(set(out_iface_del) - set(out_iface_add)) - - config.update( - { - 'in_iface_add': final_in_iface_add, - 'in_iface_del': final_in_iface_del, - 'out_iface_add': final_out_iface_add, - 'out_iface_del': final_out_iface_del, - 'changed_rules': changed_rules, - } - ) - - if effective_config: - config.update({'effective': effective_config}) - - return config - - -def verify(config): - if 'remove' in config: - return None - - required_keys = {'inside_interface', 'outside_interface'} - for rule, rule_config in config['rule'].items(): - missing_keys = required_keys - rule_config.keys() - if missing_keys: - raise ConfigError( - f"Required options are missing: {', '.join(missing_keys).replace('_', '-')} in rule {rule}" - ) - - if not rule_config.get('local', {}).get('address'): - raise ConfigError(f'Local settings require address in rule {rule}') - - if not rule_config.get('external', {}).get('address'): - raise ConfigError(f'External settings require address in rule {rule}') - - has_local_port = 'port' in rule_config.get('local', {}) - has_external_port = 'port' in rule_config.get('external', {}) - - if not has_external_port == has_local_port: - raise ConfigError( - 'Source and destination ports must either both be specified, or neither must be specified' - ) - - -def generate(config): - pass - - -def apply(config): - n = Nat44Static() - - # Delete inside interfaces - for interface in config['in_iface_del']: - n.delete_inside_interface(interface) - # Delete outside interfaces - for interface in config['out_iface_del']: - n.delete_outside_interface(interface) - # Delete NAT static mapping rules - for rule in config['changed_rules']: - if rule in config.get('effective', {}).get('rule', {}): - rule_config = config['effective']['rule'][rule] - n.delete_nat44_static_mapping( - local_ip=rule_config.get('local').get('address'), - external_ip=rule_config.get('external', {}).get('address', ''), - local_port=int(rule_config.get('local', {}).get('port', 0)), - external_port=int(rule_config.get('external', {}).get('port', 0)), - protocol=protocol_map[rule_config.get('protocol', 'all')], - ) - - if 'remove' in config: - return None - - # Add NAT44 static mapping rules - n.enable_nat44_ed() - for interface in config['in_iface_add']: - n.add_inside_interface(interface) - for interface in config['out_iface_add']: - n.add_outside_interface(interface) - for rule in config['changed_rules']: - if rule in config.get('rule', {}): - rule_config = config['rule'][rule] - n.add_nat44_static_mapping( - local_ip=rule_config.get('local').get('address'), - external_ip=rule_config.get('external', {}).get('address', ''), - local_port=int(rule_config.get('local', {}).get('port', 0)), - external_port=int(rule_config.get('external', {}).get('port', 0)), - protocol=protocol_map[rule_config.get('protocol', 'all')], - ) - - -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_nat44.py b/src/op_mode/show_vpp_nat44.py index fcf28f76a..d0569ac49 100644 --- a/src/op_mode/show_vpp_nat44.py +++ b/src/op_mode/show_vpp_nat44.py @@ -33,6 +33,15 @@ protocol_map = { 17: 'udp', } +# NAT flags +flags_map = { + 'twice-nat': 0x01, + 'self-twice-nat': 0x02, + 'out2in-only': 0x04, + 'out': 0x10, + 'in': 0x20, +} + def _verify(func): """Decorator checks if config for VPP NAT44 exists""" @@ -50,6 +59,16 @@ def _verify(func): return _wrapper +def decode_bitmask(bitmask: int) -> list: + """Decode a bitmask into a list of flag names""" + return [name for name, value in flags_map.items() if bitmask & value] + + +def _get_raw_output(data_dump): + data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump] + return data + + def _get_raw_output_sessions(vpp_api): users: list[dict] = vpp_api.nat44_user_dump() sessions_list: list[dict] = [] @@ -108,12 +127,30 @@ def _get_formatted_output_sessions(sessions_list): print('\n') -def _get_raw_output_static_rules(vpp_api): - nat_static_dump = vpp_api.nat44_static_mapping_dump() - rules_list = [ - json.loads(json.dumps(rule._asdict(), default=str)) for rule in nat_static_dump - ] - return rules_list +def _get_formatted_output_addresses(addresses): + twice_nat_address = [] + translation_address = [] + for address_info in addresses: + address = address_info.get('ip_address') + if address_info.get('flags') & flags_map['twice-nat']: + twice_nat_address.append(address) + else: + translation_address.append(address) + + print('NAT44 pool addresses:') + for addr in translation_address: + print(f' {addr}') + print('NAT44 twice-nat pool addresses:') + for addr in twice_nat_address: + print(f' {addr}') + + +def _get_formatted_output_interfaces(vpp, interfaces): + print('NAT44 interfaces:') + for interface in interfaces: + name = vpp.get_interface_name(interface['sw_if_index']) + iface_type = decode_bitmask(interface['flags']) + print(f' {name} {" ".join(iface_type)}') def _get_formatted_output_rules(rules_list): @@ -124,8 +161,16 @@ def _get_formatted_output_rules(rules_list): local_address = rule.get('local_ip_address') local_port = rule.get('local_port') or '' protocol = protocol_map[rule.get('protocol', 0)] - - values = [external_address, external_port, local_address, local_port, protocol] + options = ' '.join(decode_bitmask(rule.get('flags'))) + + values = [ + external_address, + external_port, + local_address, + local_port, + protocol, + options, + ] data_entries.append(values) headers = [ 'External address', @@ -133,6 +178,7 @@ def _get_formatted_output_rules(rules_list): 'Local address', 'Local port', 'Protocol', + 'Options', ] out = sorted(data_entries, key=lambda x: x[2]) return tabulate(out, headers=headers, tablefmt='simple') @@ -159,7 +205,8 @@ def show_summary(raw: bool): @_verify def show_static(raw: bool): vpp = VPPControl() - rules_list: list[dict] = _get_raw_output_static_rules(vpp.api) + nat_static_dump = vpp.api.nat44_static_mapping_dump() + rules_list: list[dict] = _get_raw_output(nat_static_dump) if raw: return rules_list @@ -168,6 +215,32 @@ def show_static(raw: bool): return _get_formatted_output_rules(rules_list) +@_verify +def show_addresses(raw: bool): + vpp = VPPControl() + addresses_dump = vpp.api.nat44_address_dump() + addresses: list[dict] = _get_raw_output(addresses_dump) + + if raw: + return addresses + + else: + return _get_formatted_output_addresses(addresses) + + +@_verify +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.nat44_interface_dump() + interfaces: list[dict] = _get_raw_output(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) -- cgit v1.2.3 From 528248af9628c3170b4eac399f0bb339072d8eae Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Thu, 1 May 2025 20:15:32 +0300 Subject: T7390: VPP CGNAT implementation (#30) CLI: ``` set vpp nat cgnat interface outside # multi set vpp nat cgnat interface inside # multi set vpp nat cgnat rule outside-prefix set vpp nat cgnat rule inside-prefix set vpp nat cgnat timeout udp # default 300 set vpp nat cgnat timeout tcp-established # default 7440 set vpp nat cgnat timeout tcp-transitory # default 240 set vpp nat cgnat timeout icmp # default 60 ``` OP mode: ``` show vpp nat cgnat interfaces show vpp nat cgnat mappings show vpp nat cgnat sessions clear vpp cgnat inside-address
port external-address
port ``` --- data/config-mode-dependencies/vyos-vpp.json | 8 ++ data/templates/vpp/startup.conf.j2 | 1 + interface-definitions/vpp.xml.in | 127 ++++++++++++++++- op-mode-definitions/vpp_nat_cgnat.xml.in | 100 +++++++++++++ python/vyos/vpp/nat/__init__.py | 3 +- python/vyos/vpp/nat/det44.py | 106 ++++++++++++++ src/conf_mode/vpp.py | 2 + src/conf_mode/vpp_nat.py | 12 +- src/conf_mode/vpp_nat_cgnat.py | 211 ++++++++++++++++++++++++++++ src/op_mode/vpp_nat_cgnat.py | 140 ++++++++++++++++++ 10 files changed, 705 insertions(+), 5 deletions(-) create mode 100644 op-mode-definitions/vpp_nat_cgnat.xml.in create mode 100644 python/vyos/vpp/nat/det44.py create mode 100644 src/conf_mode/vpp_nat_cgnat.py create mode 100644 src/op_mode/vpp_nat_cgnat.py (limited to 'python') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 46c1ee9f2..0f1d8af34 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -11,41 +11,49 @@ "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ethernet": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_geneve": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_gre": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ipip": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_loopback": { "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_vxlan": { "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] } } diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index 1cba80f4e..589afa67b 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -96,6 +96,7 @@ plugins { # plugin cnat_plugin.so { enable } plugin nat_plugin.so { enable } plugin nat44_ei_plugin.so { enable } + plugin det44_plugin.so { enable } # plugin nat44_ei_plugin.so { enable } # plugin nat64_plugin.so { enable } # plugin nat66_plugin.so { enable } diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index b1ae348da..368405a02 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -833,10 +833,10 @@ UDP timeout u32 - Timeout in seconds (default: 60) + Timeout in seconds (default: 300) - 60 + 300 @@ -934,6 +934,129 @@ + + + Network Address Translation (NAT) settings + + + + + Carrier-grade NAT (CGNAT) + 321 + + + + + CGNAT interface setting + + + + + CGNAT inside interface + + + + + + + + + CGNAT outside interface + + + + + + + + + + + Rule number for CGNAT + + u32 + Number of rule + + + + + + Inside IPv4 prefix + + ipv4net + IPv4 prefix + + + + + + + + + Outside IPv4 prefix + + ipv4net + IPv4 prefix + + + + + + + #include + + + + + Timeouts for CGNAT sessions + + + + + ICMP timeout + + u32 + Timeout in seconds (default: 60) + + + 60 + + + + TCP established timeout + + u32 + TCP established timeout in seconds (default: 7440) + + + 7440 + + + + TCP transitory timeout + + u32 + Timeout in seconds (default: 240) + + + 240 + + + + UDP timeout + + u32 + Timeout in seconds (default: 300) + + + 300 + + + + + + + NAT44 diff --git a/op-mode-definitions/vpp_nat_cgnat.xml.in b/op-mode-definitions/vpp_nat_cgnat.xml.in new file mode 100644 index 000000000..8af10711f --- /dev/null +++ b/op-mode-definitions/vpp_nat_cgnat.xml.in @@ -0,0 +1,100 @@ + + + + + + + + + Show VPP NAT information + + + + + Show VPP CGNAT information + + + + + Show VPP CGNAT mappings + + sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_mappings + + + + Show VPP CGNAT sessions + + sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_sessions + + + + Show VPP CGNAT interfaces + + sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_interfaces + + + + + + + + + + + + + + Terminate VPP NAT sessions + + + + + Terminate VPP CGNAT sessions + + + + + Inside IP address + + <x.x.x.x> + + + + + + Port + + 0-65535 + + + + + + External IP address + + <x.x.x.x> + + + + + + Port + + 0-65535 + + + sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py clear_session --address $5 --port $7 --ext-address $9 --ext-port ${11} + + + + + + + + + + + + + + diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py index bbd011435..16685c9cc 100644 --- a/python/vyos/vpp/nat/__init__.py +++ b/python/vyos/vpp/nat/__init__.py @@ -1,3 +1,4 @@ from .nat44 import Nat44 +from .det44 import Det44 -__all__ = ['Nat44'] +__all__ = ['Nat44', 'Det44'] diff --git a/python/vyos/vpp/nat/det44.py b/python/vyos/vpp/nat/det44.py new file mode 100644 index 000000000..1046d7cf9 --- /dev/null +++ b/python/vyos/vpp/nat/det44.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2025 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 + + +class Det44: + def __init__(self): + self.vpp = VPPControl() + + def enable_det44_plugin(self): + """Enable DET44 plugin + Example: + from vyos.vpp.nat import Det44 + det44 = Det44() + det44.enable_det44_plugin() + https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/det44/det44.api + """ + self.vpp.api.det44_plugin_enable_disable(enable=True) + + def disable_det44_plugin(self): + """Disable DET44 plugin""" + self.vpp.api.det44_plugin_enable_disable(enable=False) + + def add_det44_interface_outside(self, interface_out): + """Add DET44 outside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_inside=False, + is_add=True, + ) + + def delete_det44_interface_outside(self, interface_out): + """Delete DET44 outside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_inside=False, + is_add=False, + ) + + def add_det44_interface_inside(self, interface_in): + """Add DET44 inside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_inside=True, + is_add=True, + ) + + def delete_det44_interface_inside(self, interface_in): + """Delete DET44 inside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_inside=True, + is_add=False, + ) + + def add_det44_mapping(self, in_addr, in_plen, out_addr, out_plen): + """Add DET44 mapping""" + self.vpp.api.det44_add_del_map( + in_addr=in_addr, + in_plen=in_plen, + out_addr=out_addr, + out_plen=out_plen, + is_add=True, + ) + + def delete_det44_mapping(self, in_addr, in_plen, out_addr, out_plen): + """Delete DET44 mapping""" + self.vpp.api.det44_add_del_map( + in_addr=in_addr, + in_plen=in_plen, + out_addr=out_addr, + out_plen=out_plen, + is_add=False, + ) + + def set_det44_timeouts( + self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int + ): + """Set DET44 timeouts + Args: + tcp_established (int): TCP established timeout + tcp_transitory (int): TCP transitory timeout + udp (int): UDP timeout + icmp (int): ICMP timeout + """ + self.vpp.api.det44_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 7f9adc68b..3718f3897 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -138,6 +138,8 @@ def get_config(config=None): # NAT dependency if conf.exists(['vpp', 'nat44']): set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) if not conf.exists(base): return { diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py index d38599388..a6bc70032 100644 --- a/src/conf_mode/vpp_nat.py +++ b/src/conf_mode/vpp_nat.py @@ -186,7 +186,11 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for "address-pool translation interface"' ) - iface_address = get_interface_address(interface).get('addr_info', [])[0].get('local') + iface_address = ( + get_interface_address(interface) + .get('addr_info', [])[0] + .get('local') + ) addresses_translation.append(iface_address) if 'twice_nat' in address_pool: @@ -211,7 +215,11 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for "address-pool twice-nat interface"' ) - iface_address = get_interface_address(interface).get('addr_info', [])[0].get('local') + iface_address = ( + get_interface_address(interface) + .get('addr_info', [])[0] + .get('local') + ) addresses_twice_nat.append(iface_address) if 'static' in config: diff --git a/src/conf_mode/vpp_nat_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py new file mode 100644 index 000000000..05260c476 --- /dev/null +++ b/src/conf_mode/vpp_nat_cgnat.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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.config import Config, config_dict_merge +from vyos.configdict import node_changed +from vyos.configdiff import Diff +from vyos.vpp.utils import cli_ifaces_list + +from vyos.vpp.nat.det44 import Det44 + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat', 'cgnat'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # Get effective config as we need full dictionary to delete + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + # Get default values which we need to conditionally update into the + # dictionary retrieved. + default_values = conf.get_config_defaults(**config.kwargs, recursive=True) + config = config_dict_merge(default_values, config) + + config_changed = node_changed( + conf, + base, + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_rules = node_changed( + conf, + base + ['rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + if not config_changed: + changed_rules = list(config.get('rule', {}).keys()) + + config.update( + { + 'changed_rules': changed_rules, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + if 'interface' not in config: + raise ConfigError('Interfaces must be configured for CGNAT') + if 'rule' not in config: + raise ConfigError('Rules must be configured for CGNAT') + + required_keys = {'inside', 'outside'} + missing_keys = required_keys - set(config['interface'].keys()) + if missing_keys: + raise ConfigError( + f'Both inside and outside interfaces must be configured. ' + f'Please add: {", ".join(missing_keys)}' + ) + + conflict_ifaces = set(config['interface']['inside']).intersection( + set(config['interface']['outside']) + ) + if conflict_ifaces: + raise ConfigError( + f'Interface cannot be both inside and outside. ' + f'Please choose a side for: {", ".join(conflict_ifaces)} ' + ) + + for interface in config['interface']['inside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for inside CGNAT interface' + ) + for interface in config['interface']['outside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for outside CGNAT interface' + ) + + required_keys = {'outside_prefix', 'inside_prefix'} + for rule in config['rule']: + missing_keys = required_keys - set(config['rule'][rule].keys()) + if missing_keys: + raise ConfigError( + f'Both inside-prefix and outside-prefix must be configured in rule {rule}. ' + f'Please add: {", ".join(missing_keys).replace("_", "-")}' + ) + + +def generate(config): + pass + + +def apply(config): + cgnat = Det44() + + if 'remove' in config: + cgnat.disable_det44_plugin() + return None + + if 'effective' in config: + remove_config = config.get('effective') + # Delete inside interfaces + for interface in remove_config['interface']['inside']: + if interface not in config.get('interface', {}).get('inside', []): + cgnat.delete_det44_interface_inside(interface) + # Delete outside interfaces + for interface in remove_config['interface']['outside']: + if interface not in config.get('interface', {}).get('outside', []): + cgnat.delete_det44_interface_outside(interface) + # Delete CGNAT rules + for rule in config['changed_rules']: + if rule in remove_config.get('rule', {}): + rule_config = remove_config['rule'][rule] + in_addr, in_plen = rule_config['inside_prefix'].split('/') + out_addr, out_plen = rule_config['outside_prefix'].split('/') + cgnat.delete_det44_mapping( + in_addr=in_addr, + in_plen=int(in_plen), + out_addr=out_addr, + out_plen=int(out_plen), + ) + + # Add DET44 + cgnat.enable_det44_plugin() + # Add inside interfaces + for interface in config['interface']['inside']: + cgnat.add_det44_interface_inside(interface) + # Add outside interfaces + for interface in config['interface']['outside']: + cgnat.add_det44_interface_outside(interface) + # Add CGNAT rules + for rule in config['changed_rules']: + if rule in config.get('rule', {}): + rule_config = config['rule'][rule] + in_addr, in_plen = rule_config['inside_prefix'].split('/') + out_addr, out_plen = rule_config['outside_prefix'].split('/') + cgnat.add_det44_mapping( + in_addr=in_addr, + in_plen=int(in_plen), + out_addr=out_addr, + out_plen=int(out_plen), + ) + # Set CGNAT timeouts + cgnat.set_det44_timeouts( + icmp=int(config['timeout']['icmp']), + udp=int(config['timeout']['udp']), + tcp_established=int(config['timeout']['tcp_established']), + tcp_transitory=int(config['timeout']['tcp_transitory']), + ) + + +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/vpp_nat_cgnat.py b/src/op_mode/vpp_nat_cgnat.py new file mode 100644 index 000000000..5112c70be --- /dev/null +++ b/src/op_mode/vpp_nat_cgnat.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 json +import sys +from tabulate import tabulate + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +def _verify(func): + """Decorator checks if config for VPP NAT CGNAT exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + base = 'vpp nat cgnat' + if not config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured') + + return func(*args, **kwargs) + + return _wrapper + + +def _get_raw_output(data_dump): + data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump] + return data + + +def _get_formatted_output_interfaces(vpp, interfaces): + print('CGNAT interfaces:') + for interface in interfaces: + name = vpp.get_interface_name(interface['sw_if_index']) + iface_type = 'in' if interface['is_inside'] else 'out' + print(f' {name} {iface_type}') + + +def _get_formatted_output_mappings(rules_list): + data_entries = [] + for rule in rules_list: + in_addr = rule.get('in_addr') + in_plen = str(rule.get('in_plen')) + out_addr = rule.get('out_addr') + out_plen = str(rule.get('out_plen')) + sharing_ratio = rule.get('sharing_ratio') + ports_per_host = rule.get('ports_per_host') + ses_num = rule.get('ses_num') + + values = [ + f'{in_addr}/{in_plen}', + f'{out_addr}/{out_plen}', + sharing_ratio, + ports_per_host, + ses_num, + ] + data_entries.append(values) + headers = [ + 'Inside', + 'Outside', + 'Sharing ratio', + 'Ports per host', + 'Sessions', + ] + out = sorted(data_entries, key=lambda x: x[0]) + return tabulate(out, headers=headers, tablefmt='simple') + + +@_verify +def show_sessions(raw: bool): + vpp = VPPControl() + out = vpp.cli_cmd('show det44 sessions').reply + out = out.replace('NAT44 deterministic', 'CGNAT') + return out + + +@_verify +def show_mappings(raw: bool): + vpp = VPPControl() + nat_static_dump = vpp.api.det44_map_dump() + rules_list: list[dict] = _get_raw_output(nat_static_dump) + + if raw: + return rules_list + + else: + return _get_formatted_output_mappings(rules_list) + + +@_verify +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.det44_interface_dump() + interfaces: list[dict] = _get_raw_output(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + +@_verify +def clear_session(address: str, port: str, ext_address: str, ext_port: str): + vpp = VPPControl() + vpp.api.det44_close_session_in( + in_addr=address, + in_port=int(port), + ext_addr=ext_address, + ext_port=int(ext_port), + ) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 151afffe15ce89755f3c3f81a9d2c647e487f647 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 13 May 2025 17:33:35 +0300 Subject: T7419: VPP ACL implementation (#31) CLI: ``` set vpp acl ip tag-name rule action set vpp acl ip tag-name rule source prefix set vpp acl ip tag-name rule source port set vpp acl ip tag-name rule destination prefix set vpp acl ip tag-name rule destination port set vpp acl ip tag-name rule protocol set vpp acl ip tag-name rule tcp-flags set vpp acl ip tag-name rule tcp-flags not set vpp acl ip interface input acl-tag tag-name set vpp acl ip interface output acl-tag tag-name set vpp acl macip tag-name rule prefix set vpp acl macip tag-name rule mac-address set vpp acl macip tag-name rule mac-mask set vpp acl macip tag-name rule action set vpp acl macip interface tag-name ``` OP mode ``` show vpp acl ip tag-name show vpp acl ip interface show vpp acl macip tag-name show vpp acl macip interface ``` --- data/config-mode-dependencies/vyos-vpp.json | 8 + data/templates/vpp/startup.conf.j2 | 2 + .../include/vpp/acl_common_interface_ip_rule.xml.i | 25 ++ .../include/vpp/acl_port_range.xml.i | 18 + interface-definitions/include/vpp/acl_prefix.xml.i | 20 ++ .../include/vpp/acl_tcp_flags.xml.i | 50 +++ interface-definitions/vpp.xml.in | 254 ++++++++++++- op-mode-definitions/vpp_acl.xml.in | 65 ++++ python/vyos/vpp/acl/__init__.py | 3 + python/vyos/vpp/acl/acl.py | 106 ++++++ src/conf_mode/vpp.py | 4 + src/conf_mode/vpp_acl.py | 393 +++++++++++++++++++++ src/conf_mode/vpp_interfaces_bonding.py | 10 + src/conf_mode/vpp_interfaces_gre.py | 10 + src/conf_mode/vpp_interfaces_ipip.py | 10 + src/conf_mode/vpp_interfaces_loopback.py | 10 + src/conf_mode/vpp_interfaces_vxlan.py | 10 + src/op_mode/vpp_acl.py | 342 ++++++++++++++++++ 18 files changed, 1338 insertions(+), 2 deletions(-) create mode 100644 interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i create mode 100644 interface-definitions/include/vpp/acl_port_range.xml.i create mode 100644 interface-definitions/include/vpp/acl_prefix.xml.i create mode 100644 interface-definitions/include/vpp/acl_tcp_flags.xml.i create mode 100644 op-mode-definitions/vpp_acl.xml.in create mode 100644 python/vyos/vpp/acl/__init__.py create mode 100644 python/vyos/vpp/acl/acl.py create mode 100644 src/conf_mode/vpp_acl.py create mode 100644 src/op_mode/vpp_acl.py (limited to 'python') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 0f1d8af34..2a5613559 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -10,41 +10,48 @@ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"], "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ethernet": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_geneve": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_gre": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ipip": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_loopback": { + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] @@ -52,6 +59,7 @@ "vpp_interfaces_vxlan": { "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index 589afa67b..79a551338 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -114,6 +114,8 @@ plugins { plugin crypto_openssl_plugin.so { enable } {% endif %} # plugin wireguard_plugin.so { enable } + # ACL + plugin acl_plugin.so { enable } } linux-cp { diff --git a/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i new file mode 100644 index 000000000..a719a6223 --- /dev/null +++ b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i @@ -0,0 +1,25 @@ + + + + ACL rule (tag) number + + u32 + Number + + + + + Number must be between 1 and 4294967295 + + + + + ACL tag name + + vpp acl ip tag-name + + + + + + diff --git a/interface-definitions/include/vpp/acl_port_range.xml.i b/interface-definitions/include/vpp/acl_port_range.xml.i new file mode 100644 index 000000000..26bd2de45 --- /dev/null +++ b/interface-definitions/include/vpp/acl_port_range.xml.i @@ -0,0 +1,18 @@ + + + + Port number or range + + u32:1-65535 + Numeric IP port + + + range + Numbered port range (e.g., 1001-1005) + + + + + + + diff --git a/interface-definitions/include/vpp/acl_prefix.xml.i b/interface-definitions/include/vpp/acl_prefix.xml.i new file mode 100644 index 000000000..790153c8f --- /dev/null +++ b/interface-definitions/include/vpp/acl_prefix.xml.i @@ -0,0 +1,20 @@ + + + + IP prefix + + ipv4net + IPv4 prefix + + + ipv6net + IPv6 prefix + + + + + + + + + diff --git a/interface-definitions/include/vpp/acl_tcp_flags.xml.i b/interface-definitions/include/vpp/acl_tcp_flags.xml.i new file mode 100644 index 000000000..da17f3fe5 --- /dev/null +++ b/interface-definitions/include/vpp/acl_tcp_flags.xml.i @@ -0,0 +1,50 @@ + + + + Synchronise flag + + + + + + Acknowledge flag + + + + + + Finish flag + + + + + + Reset flag + + + + + + Urgent flag + + + + + + Push flag + + + + + + Explicit Congestion Notification flag + + + + + + Congestion Window Reduced flag + + + + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 368405a02..dab0ea308 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -942,7 +942,7 @@ Carrier-grade NAT (CGNAT) - 321 + 331 @@ -1060,7 +1060,7 @@ NAT44 - 320 + 330 @@ -1265,6 +1265,256 @@ + + + Access Control Lists (ACLs) + 332 + + + + + Access Control List by IPv4/IPv6 + + + + + ACL tag name + + txt + Tag name + + + + #include + + + Rule number for Access control element (ACE) + + u32 + Number of ACE + + + + + Rule number must be between 1 and 4294967295 + + + #include + + + Rule action + + permit deny permit-reflect + + + permit + Permit matching traffic + + + deny + Deny matching traffic + + + permit-reflect + Permit the matching outbound traffic and allow the reverse traffic + + + (permit|deny|permit-reflect) + + + + + + Source parameters + + + #include + #include + + + + + Destination parameters + + + #include + #include + + + + + Protocol + + + all + + + all + All IP protocols + + + <protocol> + IP protocol name + + + + + + all + + + + TCP flags + + + #include + + + Match flags not set + + + #include + + + + + + + + + + + Apply an ACL to an interface + + + + + + + + Input direction + + + #include + + + + + Output direction + + + #include + + + + + + + + + Access Control List by mac address + + + + + ACL tag name + + txt + ACL name + + + + #include + + + Rule number for Access control element (ACE) + + u32 + Number of ACE + + + + + Rule number must be between 1 and 4294967295 + + + #include + + + Rule action + + permit deny + + + permit + Permit matching traffic + + + deny + Deny matching traffic + + + (permit|deny) + + + + #include + + + Source IP prefix + + + + + Source MAC address + + macaddr + MAC address + + + + + + + + + Source MAC mask (default ff:ff:ff:ff:ff:ff) + + macaddr + MAC mask + + + + + + ff:ff:ff:ff:ff:ff + + + + + + + + Apply an ACL to an input interface + + + + + + + + ACL tag name + + vpp acl macip tag-name + + + + + + + + + VPP kernel interface settings diff --git a/op-mode-definitions/vpp_acl.xml.in b/op-mode-definitions/vpp_acl.xml.in new file mode 100644 index 000000000..a901a537d --- /dev/null +++ b/op-mode-definitions/vpp_acl.xml.in @@ -0,0 +1,65 @@ + + + + + + + + + Show VPP ACL information + + + + + Show VPP ACL by IPv4/IPv6 + + + + + Show specified VPP ACL + + vpp acl ip tag-name + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls --tag-name="$6" + + + + Show VPP ACL interfaces + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_interfaces + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls + + + + Show VPP ACL by macip + + + + + Show specified VPP ACL + + vpp acl macip tag-name + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls --tag-name="$6" + + + + Show VPP ACL interfaces + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_interfaces + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_all_acls + + + + + + diff --git a/python/vyos/vpp/acl/__init__.py b/python/vyos/vpp/acl/__init__.py new file mode 100644 index 000000000..6bd6cdb23 --- /dev/null +++ b/python/vyos/vpp/acl/__init__.py @@ -0,0 +1,3 @@ +from .acl import Acl + +__all__ = ['Acl'] diff --git a/python/vyos/vpp/acl/acl.py b/python/vyos/vpp/acl/acl.py new file mode 100644 index 000000000..9da241f1c --- /dev/null +++ b/python/vyos/vpp/acl/acl.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2025 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 + + +NO_ACL_INDEX = 0xFFFFFFFF + + +class Acl: + def __init__(self): + self.vpp = VPPControl() + + def get_acl_index_by_tag(self, tag): + """Get ACL index by tag name + https://github.com/FDio/vpp/blob/21c641f9356da5137760cdc799127064c8c1fd31/src/plugins/acl/acl.api + """ + for acl in self.vpp.api.acl_dump(acl_index=NO_ACL_INDEX): + if acl.tag == tag: + return acl.acl_index + return NO_ACL_INDEX + + def add_replace_acl(self, tag, rules): + """Add new ACL or replace existing one""" + self.vpp.api.acl_add_replace( + tag=tag, + acl_index=self.get_acl_index_by_tag(tag), + count=len(rules), + r=rules, + ) + + def delete_acl(self, tag): + """Delete existing ACL""" + self.vpp.api.acl_del(acl_index=self.get_acl_index_by_tag(tag)) + + def add_acl_interface(self, interface, input_tags, output_tags): + """Add or replace ACLs on interface""" + acls = [] + for tag in input_tags: + acl_index = self.get_acl_index_by_tag(tag) + acls.append(acl_index) + for tag in output_tags: + acl_index = self.get_acl_index_by_tag(tag) + acls.append(acl_index) + self.vpp.api.acl_interface_set_acl_list( + sw_if_index=self.vpp.get_sw_if_index(interface), + count=len(acls), + n_input=len(input_tags), + acls=acls, + ) + + def delete_acl_interface(self, interface): + """Delete ACLs from interface""" + self.vpp.api.acl_interface_set_acl_list( + sw_if_index=self.vpp.get_sw_if_index(interface), + count=0, + ) + + def get_macip_acl_index_by_tag(self, tag): + """Get macip ACL by tag name""" + for acl in self.vpp.api.macip_acl_dump(): + if acl.tag == tag: + return acl.acl_index + return NO_ACL_INDEX + + def add_replace_acl_macip(self, tag, rules): + """Add or replace existing macip ACL""" + self.vpp.api.macip_acl_add_replace( + tag=tag, + acl_index=self.get_macip_acl_index_by_tag(tag), + count=len(rules), + r=rules, + ) + + def delete_acl_macip(self, tag): + """Delete existing macip ACL""" + self.vpp.api.macip_acl_del(acl_index=self.get_macip_acl_index_by_tag(tag)) + + def add_acl_macip_interface(self, interface, tag): + """Add or replace macip ACLs on interface""" + self.vpp.api.macip_acl_interface_add_del( + sw_if_index=self.vpp.get_sw_if_index(interface), + acl_index=self.get_macip_acl_index_by_tag(tag), + is_add=True, + ) + + def delete_acl_macip_interface(self, interface): + """Delete macip ACLs from interface""" + self.vpp.api.macip_acl_interface_add_del( + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=False, + ) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 3718f3897..ed0e80c2c 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -141,6 +141,10 @@ def get_config(config=None): if conf.exists(['vpp', 'nat', 'cgnat']): set_dependents('vpp_nat_cgnat', conf) + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + if not conf.exists(base): return { 'removed_ifaces': removed_ifaces, diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py new file mode 100644 index 000000000..28eae485b --- /dev/null +++ b/src/conf_mode/vpp_acl.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 ipaddress + +from vyos import ConfigError + +from vyos.configdiff import Diff +from vyos.configdict import node_changed +from vyos.config import Config +from vyos.utils.network import get_protocol_by_name + +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp.acl import Acl + + +# TCP flag names to bit values +TCP_FLAGS = { + 'FIN': 0x01, + 'SYN': 0x02, + 'RST': 0x04, + 'PSH': 0x08, + 'ACK': 0x10, + 'URG': 0x20, + 'ECN': 0x40, + 'CWR': 0x80, +} + +# ACL action flags +action_map = { + 'deny': 0, + 'permit': 1, + 'permit-reflect': 2, +} + + +def get_tcp_mask_value(set_flags, unset_flags): + mask = 0 + value = 0 + + for flag in set_flags + unset_flags: + bit = TCP_FLAGS.get(flag.upper()) + mask |= bit + if flag in set_flags: + value |= bit + + return mask, value + + +def get_port_first_last(port_range, protocol): + first_port = 0 + last_port = 65535 + if not port_range: + if protocol in ['icmp', 'ipv6-icmp']: + last_port = 255 + elif '-' not in port_range: + first_port = last_port = port_range + else: + first_port, last_port = port_range.split('-') + return int(first_port), int(last_port) + + +def create_ip_rules_list(rules): + rules_list = [] + for rule in rules.values(): + r = { + 'is_permit': action_map[rule.get('action')], + 'src_prefix': rule.get('source', {}).get('prefix', ''), + 'dst_prefix': rule.get('destination', {}).get('prefix', ''), + 'proto': ( + int(get_protocol_by_name(rule.get('protocol'))) + if rule.get('protocol') != 'all' + else 0 + ), + } + + tcp_flags = rule.get('tcp_flags', {}) + set_flags = [flag for flag in tcp_flags if flag != 'not'] + unet_flags = list(tcp_flags.get('not', {}).keys()) + tcp_mask, tcp_value = get_tcp_mask_value(set_flags, unet_flags) + r['tcp_flags_mask'] = tcp_mask + r['tcp_flags_value'] = tcp_value + + src_ports = rule.get('source', {}).get('port') + src_first_port, src_last_port = get_port_first_last( + src_ports, rule.get('protocol') + ) + r['srcport_or_icmptype_first'] = src_first_port + r['srcport_or_icmptype_last'] = src_last_port + + dst_ports = rule.get('destination', {}).get('port') + dst_first_port, dst_last_port = get_port_first_last( + dst_ports, rule.get('protocol') + ) + r['dstport_or_icmpcode_first'] = dst_first_port + r['dstport_or_icmpcode_last'] = dst_last_port + + rules_list.append(r) + + return rules_list + + +def create_macip_rules_list(rules): + rules_list = [] + for rule in rules.values(): + r = { + 'is_permit': action_map[rule.get('action')], + 'src_prefix': rule.get('prefix', ''), + 'src_mac': rule.get('mac_address', ''), + 'src_mac_mask': rule.get('mac_mask', ''), + } + rules_list.append(r) + + return rules_list + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'acl'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + changed_ip_ifaces = node_changed( + conf, + base + ['ip', 'interface'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_macip_ifaces = node_changed( + conf, + base + ['macip', 'interface'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + config.update( + { + 'changed_ip_ifaces': changed_ip_ifaces, + 'changed_macip_ifaces': changed_macip_ifaces, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + for acl_type in ['ip', 'macip']: + if acl_type in config: + acl = config.get(acl_type) + if 'tag_name' not in acl: + raise ConfigError(f'"tag-name" is required for "acl {acl_type}"') + + for acl_name, acl_config in acl.get('tag_name').items(): + if 'rule' not in acl_config: + raise ConfigError(f'Rules must be configured for ACL {acl_name}') + + for rule, rule_config in acl_config.get('rule').items(): + err_msg = f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:' + if 'action' not in rule_config: + raise ConfigError(f'{err_msg} action must be defined') + + for iface, iface_config in acl.get('interface', {}).items(): + if iface not in config.get('vpp_ifaces'): + raise ConfigError( + f'{iface} must be a VPP interface for ACL interface' + ) + + if 'ip' in config: + acl = config.get('ip') + for acl_name, acl_config in acl.get('tag_name').items(): + for rule, rule_config in acl_config.get('rule').items(): + err_msg = ( + f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:' + ) + + # verify IPv4 and IPv6 address family + src_prefix = rule_config.get('source', {}).get('prefix') + dst_prefix = rule_config.get('destination', {}).get('prefix') + src = ipaddress.ip_network(src_prefix) if src_prefix else None + dst = ipaddress.ip_network(dst_prefix) if dst_prefix else None + + if src and dst: + if src.version != dst.version: + raise ConfigError( + f'{err_msg} source and destination prefixes must be from the same IP family' + ) + elif src or dst: + family = src.version if src else dst.version + if family == 6: + raise ConfigError( + f'{err_msg} both source and destination prefixes must be defined for IPv6' + ) + + # verify protocol + protocol = rule_config.get('protocol') + if protocol != 'all': + proto = get_protocol_by_name(protocol) + if not isinstance(proto, int) and ( + not proto.isdigit() or int(proto) > 147 + ): + raise ConfigError( + f'{err_msg} protocol name {protocol} is not valid' + ) + + # verify TCP flags + if 'tcp_flags' in rule_config: + if rule_config.get('protocol') != 'tcp': + raise ConfigError( + f'{err_msg} protocol must be tcp when specifying tcp flags' + ) + + not_flags = rule_config.get('tcp_flags').get('not', []) + if not_flags: + duplicates = [ + flag + for flag in rule_config.get('tcp_flags') + if flag in not_flags + ] + if duplicates: + raise ConfigError( + f'{err_msg} cannot match a tcp flag as set and not set: {duplicates}' + ) + + for iface, iface_config in acl.get('interface', {}).items(): + if not any(key in iface_config for key in ('input', 'output')): + raise ConfigError( + f'Please specify direction input/output for interface {iface}' + ) + + for direction in ['input', 'output']: + if direction in iface_config: + iface_acl = iface_config.get(direction) + if 'acl_tag' not in iface_acl: + raise ConfigError( + f'"acl-tag" is required for {direction} interface {iface}' + ) + + used_names = [] + for tag, tag_conf in iface_acl.get('acl_tag').items(): + if 'tag_name' not in tag_conf: + raise ConfigError( + f'"tag-name" is required for {direction} interface {iface} with acl-tag {tag}' + ) + name = tag_conf.get('tag_name') + if name not in acl.get('tag_name').keys(): + raise ConfigError( + f'ACL with tag-name {name} does not exist. ' + f'Cannot use it for {direction} interface {iface}' + ) + if name in used_names: + raise ConfigError( + f'ACL with tag-name {name} is already used for {direction} interface {iface}' + ) + used_names.append(name) + + if 'macip' in config: + acl = config.get('macip') + for iface, iface_config in acl.get('interface', {}).items(): + if 'tag_name' not in iface_config: + raise ConfigError(f'"tag-name" is required for interface {iface}') + name = iface_config.get('tag_name') + if name not in acl.get('tag_name').keys(): + raise ConfigError( + f'ACL with tag-name {name} does not exist. Cannot use it for interface {iface}' + ) + + +def generate(config): + pass + + +def apply(config): + acl = Acl() + + if 'effective' in config: + # Delete ACL ip + if 'ip' in config.get('effective'): + remove_config_ip = config.get('effective').get('ip') + + # Delete ACL interfaces + for interface in config.get('changed_ip_ifaces'): + acl.delete_acl_interface(interface) + + # Delete ACLs + for acl_name in remove_config_ip.get('tag_name'): + if acl_name not in config.get('ip', {}).get('tag_name', {}): + acl.delete_acl(acl_name) + + # Delete ACL macip + if 'macip' in config.get('effective'): + remove_config_macip = config.get('effective').get('macip') + + # Delete ACL interfaces + for interface in config.get('changed_macip_ifaces'): + acl.delete_acl_macip_interface(interface) + + # Delete ACL macip + for acl_name in remove_config_macip.get('tag_name'): + if acl_name not in config.get('macip', {}).get('tag_name', {}): + acl.delete_acl_macip(acl_name) + + if 'remove' in config: + return None + + # Add or replace ACL ip + config_ip = config.get('ip', {}) + for acl_name in config_ip.get('tag_name', {}): + rules = create_ip_rules_list( + config_ip.get('tag_name').get(acl_name).get('rule') + ) + acl.add_replace_acl(acl_name, rules) + + for iface, iface_config in config_ip.get('interface', {}).items(): + input_tags = [ + v['tag_name'] + for v in iface_config.get('input', {}).get('acl_tag', {}).values() + ] + output_tags = [ + v['tag_name'] + for v in iface_config.get('output', {}).get('acl_tag', {}).values() + ] + acl.add_acl_interface(iface, input_tags, output_tags) + + # Add or replace ACL macip + config_macip = config.get('macip', {}) + for acl_name in config_macip.get('tag_name', {}): + rules = create_macip_rules_list( + config_macip.get('tag_name').get(acl_name).get('rule') + ) + acl.add_replace_acl_macip(acl_name, rules) + + for iface, iface_config in config_macip.get('interface', {}).items(): + acl.add_acl_macip_interface(iface, iface_config.get('tag_name')) + + +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 index 7ab1abb24..2b17acb24 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -139,6 +139,16 @@ def get_config(config=None) -> dict: if conf.exists(['vpp', 'kernel-interfaces', iface]): set_dependents('vpp_kernel_interface', conf, iface) + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + config['ifname'] = ifname return config diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index 823f3ff4d..f6da2d55d 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -110,6 +110,16 @@ def get_config(config=None) -> dict: if conf.exists(['vpp', 'kernel-interfaces', iface]): set_dependents('vpp_kernel_interface', conf, iface) + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + config['ifname'] = ifname return config diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py index c52e7c997..f40a4e243 100644 --- a/src/conf_mode/vpp_interfaces_ipip.py +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -109,6 +109,16 @@ def get_config(config=None) -> dict: if iface in config.get('vpp_kernel_interfaces'): set_dependents('vpp_kernel_interface', conf, iface) + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + config['ifname'] = ifname return config diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py index f0bcdfb18..2b37b5e1c 100644 --- a/src/conf_mode/vpp_interfaces_loopback.py +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -99,6 +99,16 @@ def get_config(config=None) -> dict: if conf.exists(['vpp', 'kernel-interfaces', iface]): set_dependents('vpp_kernel_interface', conf, iface) + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + config['ifname'] = ifname return config diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py index 963cd90ec..065a8eafb 100644 --- a/src/conf_mode/vpp_interfaces_vxlan.py +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -116,6 +116,16 @@ def get_config(config=None) -> dict: if conf.exists(['vpp', 'kernel-interfaces', iface]): set_dependents('vpp_kernel_interface', conf, iface) + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + config['ifname'] = ifname return config diff --git a/src/op_mode/vpp_acl.py b/src/op_mode/vpp_acl.py new file mode 100644 index 000000000..7afe96433 --- /dev/null +++ b/src/op_mode/vpp_acl.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 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 json +import sys +import typing +from tabulate import tabulate + +import vyos.opmode +from vyos.config import Config +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +NO_ACL_INDEX = 0xFFFFFFFF + +# ACL action flags +action_map = { + 0: 'deny', + 1: 'permit', + 2: 'permit-reflect', +} + +# TCP flag names to bit values +TCP_FLAGS = { + 'FIN': 0x01, + 'SYN': 0x02, + 'RST': 0x04, + 'PSH': 0x08, + 'ACK': 0x10, + 'URG': 0x20, + 'ECN': 0x40, + 'CWR': 0x80, +} + + +def _verify(target): + """Decorator checks if config for VPP NAT CGNAT exists""" + from functools import wraps + + if target not in ['ip', 'macip', 'no_target']: + raise ValueError('Invalid target') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + path = 'vpp acl' + if target == 'ip': + path += ' ip' + elif target == 'macip': + path += ' macip' + if not config.exists(path): + raise vyos.opmode.UnconfiguredSubsystem(f'"{path}" is not configured') + return func(*args, **kwargs) + + return _wrapper + + return _verify_target + + +def _get_acl_tag_by_index(vpp, acl_index): + acl = vpp.api.acl_dump(acl_index=acl_index) + if acl: + return acl[0].tag + + return None + + +def _get_macip_acl_tag_by_index(vpp, acl_index): + acl = vpp.api.macip_acl_dump(acl_index=acl_index) + if acl: + return acl[0].tag + + return None + + +def _get_tcp_flag_states(value, mask): + set_flags = [] + unset_flags = [] + for flag, bit in TCP_FLAGS.items(): + if mask & bit: # This flag is being checked + if value & bit: + set_flags.append(flag) + else: + unset_flags.append(flag) + return sorted(set_flags), sorted(unset_flags) + + +def _get_raw_output_acls(data_dump): + out = [] + for data in data_dump: + rules = [json.loads(json.dumps(d._asdict(), default=str)) for d in data.r] + out.append( + { + 'acl_index': data.acl_index, + 'tag': data.tag, + 'count': data.count, + 'r': rules, + } + ) + return out + + +def _get_raw_output_interfaces(data_dump): + ifaces_list = [] + for iface in data_dump: + if iface.count != 0: + ifaces_list.append(json.loads(json.dumps(iface._asdict(), default=str))) + return ifaces_list + + +def _get_formatted_output_interfaces(vpp, interfaces): + data_entries = [] + for interface in interfaces: + name = vpp.get_interface_name(interface.get('sw_if_index')) + input_acls = [] + for acl_index in interface.get('acls')[: interface.get('n_input')]: + input_acls.append(_get_acl_tag_by_index(vpp, int(acl_index))) + output_acls = [] + for acl_index in interface.get('acls')[interface.get('n_input') :]: + output_acls.append(_get_acl_tag_by_index(vpp, int(acl_index))) + values = [ + name, + '\n'.join(input_acls), + '\n'.join(output_acls), + ] + data_entries.append(values) + + headers = ['Interface', 'Input ACLs', 'Output ACLs'] + return tabulate(data_entries, headers=headers, tablefmt='simple') + + +def _get_formatted_output_macip_interfaces(vpp, interfaces): + data_entries = [] + for interface in interfaces: + name = vpp.get_interface_name(interface.get('sw_if_index')) + acl = _get_macip_acl_tag_by_index(vpp, int(interface.get('acls')[0])) + data_entries.append([name, acl]) + + headers = ['Interface', 'ACL'] + return tabulate(data_entries, headers=headers, tablefmt='simple') + + +def _get_formatted_output_acls(acls_list): + conf = Config() + + for acl in acls_list: + acl_index = acl.get('acl_index') + tag = acl.get('tag') + rules = acl.get('r') + print( + '\n---------------------------------\n' + f'IP ACL "tag-name {tag}" acl_index {acl_index}\n' + ) + + path = ['vpp', 'acl', 'ip', 'tag-name', tag, 'rule'] + conf_rules = conf.list_nodes(path) + data_entries = [] + for rule_index, rule in enumerate(rules): + srcport_first = str(rule.get('srcport_or_icmptype_first')) + srcport_last = str(rule.get('srcport_or_icmptype_last')) + dstport_first = str(rule.get('dstport_or_icmpcode_first')) + dstport_last = str(rule.get('dstport_or_icmpcode_last')) + set_flags, unset_flags = _get_tcp_flag_states( + rule.get('tcp_flags_value'), rule.get('tcp_flags_mask') + ) + + values = [ + conf_rules[rule_index], + action_map.get(rule.get('is_permit')), + rule.get('src_prefix'), + ( + f'{srcport_first}-{srcport_last}' + if srcport_first != srcport_last + else srcport_first + ), + rule.get('dst_prefix'), + ( + f'{dstport_first}-{dstport_last}' + if dstport_first != dstport_last + else dstport_first + ), + rule.get('proto'), + '\n'.join(set_flags), + '\n'.join(unset_flags), + ] + data_entries.append(values) + + headers = [ + 'Rule', + 'Action', + 'Src prefix', + 'Src port', + 'Dst prefix', + 'Dst port', + 'Proto', + 'TCP flags set', + 'TCP flags not set', + ] + print(tabulate(data_entries, headers=headers, tablefmt='simple')) + print('\n') + + +def _get_formatted_output_macip_acls(acls_list): + conf = Config() + + for acl in acls_list: + acl_index = acl.get('acl_index') + tag = acl.get('tag') + rules = acl.get('r') + print( + '\n---------------------------------\n' + f'MACIP ACL "tag-name {tag}" acl_index {acl_index}\n' + ) + + path = ['vpp', 'acl', 'macip', 'tag-name', tag, 'rule'] + conf_rules = conf.list_nodes(path) + data_entries = [] + for rule_index, rule in enumerate(rules): + values = [ + conf_rules[rule_index], + action_map.get(rule.get('is_permit')), + rule.get('src_prefix'), + rule.get('src_mac'), + rule.get('src_mac_mask'), + ] + data_entries.append(values) + + headers = [ + 'Rule', + 'Action', + 'IP prefix', + 'MAC address', + 'MAC mask', + ] + print(tabulate(data_entries, headers=headers, tablefmt='simple')) + print('\n') + + +def _find_acl_by_tag(acls, tag_name): + return [acl for acl in acls if acl['tag'] == tag_name] + + +@_verify('ip') +def show_ip_acls(raw: bool, tag_name: typing.Optional[str]): + vpp = VPPControl() + acls_dump = vpp.api.acl_dump(acl_index=NO_ACL_INDEX) + acls: list[dict] = _get_raw_output_acls(acls_dump) + + if tag_name: + acls = _find_acl_by_tag(acls, tag_name) + + if raw: + return acls + + else: + return _get_formatted_output_acls(acls) + + +@_verify('macip') +def show_macip_acls(raw: bool, tag_name: typing.Optional[str]): + vpp = VPPControl() + acls_dump = vpp.api.macip_acl_dump(acl_index=NO_ACL_INDEX) + acls: list[dict] = _get_raw_output_acls(acls_dump) + + if tag_name: + acls = _find_acl_by_tag(acls, tag_name) + + if raw: + return acls + + else: + return _get_formatted_output_macip_acls(acls) + + +@_verify('ip') +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.acl_interface_list_dump() + interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + +@_verify('macip') +def show_macip_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.macip_acl_interface_list_dump() + interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_macip_interfaces(vpp, interfaces) + + +@_verify('no_target') +def show_all_acls(raw: bool): + conf = Config() + acls_all = {} + path = ['vpp', 'acl'] + if conf.exists(path + ['ip']): + ip_acls = show_ip_acls(raw, tag_name=None) + acls_all['ip'] = ip_acls + if conf.exists(path + ['macip']): + macip_acls = show_macip_acls(raw, tag_name=None) + acls_all['macip'] = macip_acls + + if raw: + return acls_all + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 983d296eafe97a21cf2c326aa1267edfd5308d37 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Thu, 5 Jun 2025 11:32:41 +0300 Subject: T7515: Fix VPP NAT44 timeouts --- python/vyos/vpp/control_vpp.py | 19 ------------------- python/vyos/vpp/nat/nat44.py | 9 +++++++++ smoketest/scripts/cli/test_vpp.py | 12 +++++------- src/conf_mode/vpp.py | 7 ------- src/conf_mode/vpp_nat.py | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 33 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 5db0ea560..5959b68b5 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -448,25 +448,6 @@ class VPPControl: """ self.__vpp_api_client.api.nat44_forwarding_enable_disable(enable=enable) - @_Decorators.api_call - def set_nat_timeouts( - self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int - ) -> None: - """Set NAT timeouts - - Args: - tcp_established (int): TCP established timeout - tcp_transitory (int): TCP transitory timeout - udp (int): UDP timeout - icmp (int): ICMP timeout - """ - self.__vpp_api_client.api.nat_set_timeouts( - icmp=icmp, - udp=udp, - tcp_established=tcp_established, - tcp_transitory=tcp_transitory, - ) - @_Decorators.api_call def set_nat44_session_limit(self, session_limit: int) -> None: """Set NAT44 session limit diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index e01e05153..6b02688aa 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -215,6 +215,15 @@ class Nat44: is_add=False, ) + def set_nat_timeouts(self, icmp, udp, tcp_established, tcp_transitory): + """Set NAT timeouts""" + self.vpp.api.nat_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) + def enable_ipfix(self): """Enable NAT44 IPFIX logging""" self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 7a150f7e8..d8f7f3473 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -1373,13 +1373,11 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{exclude_local_addr}:{exclude_local_port} vrf 0', out) # Check timeouts - # skipped due to https://vyos.dev/T7515 - # - # _, out = rc_cmd('sudo vppctl show nat44 ei timeouts') - # self.assertIn(f'udp timeout: {timeout_udp}sec', out) - # self.assertIn(f'tcp established timeout: {timeout_tcp_est}sec', out) - # self.assertIn(f'tcp transitory timeout: {timeout_tcp_trans}sec', out) - # self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) + _, out = rc_cmd('sudo vppctl show nat timeouts') + self.assertIn(f'udp timeout: {timeout_udp}sec', out) + self.assertIn(f'tcp-established timeout: {timeout_tcp_est}sec', out) + self.assertIn(f'tcp-transitory timeout: {timeout_tcp_trans}sec', out) + self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) # Summary _, out = rc_cmd('sudo vppctl show nat44 summary') diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index ed0e80c2c..029adbdec 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -745,13 +745,6 @@ def apply(config): enable_forwarding = False vpp_control.enable_disable_nat44_forwarding(enable_forwarding) - vpp_control.set_nat_timeouts( - icmp=int(nat44_settings.get('timeout').get('icmp')), - udp=int(nat44_settings.get('timeout').get('udp')), - tcp_established=int(nat44_settings.get('timeout').get('tcp_established')), - tcp_transitory=int(nat44_settings.get('timeout').get('tcp_transitory')), - ) - vpp_control.set_nat44_session_limit(int(nat44_settings.get('session_limit'))) if nat44_settings.get('workers'): diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py index a6bc70032..77b9400e3 100644 --- a/src/conf_mode/vpp_nat.py +++ b/src/conf_mode/vpp_nat.py @@ -104,6 +104,14 @@ def get_config(config=None) -> dict: } ) + if conf.exists(['vpp', 'settings', 'nat44', 'timeout']): + timeouts = conf.get_config_dict( + ['vpp', 'settings', 'nat44', 'timeout'], + key_mangling=('-', '_'), + with_defaults=True, + ) + config.update(timeouts) + if effective_config: config.update({'effective': effective_config}) @@ -453,6 +461,13 @@ def apply(config): port=int(rule_config.get('local_port', 0)), interface=rule_config.get('external_interface'), ) + if 'timeout' in config: + n.set_nat_timeouts( + icmp=int(config.get('timeout').get('icmp')), + udp=int(config.get('timeout').get('udp')), + tcp_established=int(config.get('timeout').get('tcp_established')), + tcp_transitory=int(config.get('timeout').get('tcp_transitory')), + ) if __name__ == '__main__': -- cgit v1.2.3 From 516ea43279d750272be398a27948049581b22630 Mon Sep 17 00:00:00 2001 From: zdc Date: Wed, 18 Jun 2025 11:51:34 +0300 Subject: XDP/Mellanox: T7223: Fixed interfaces initialization For the XDP driver and Mellanox NIC, we create `defunct_*` interfaces to hide original interfaces from CLI, when they are replaced with VPP-enabled pairs. But sometimes IP addresses are not flushed from these `defunct_*` interfaces, which leads to broken routing afterward. This commit introduces an additional flush operation for `defunct_*` interfaces to ensure that they do not conflict with VPP interfaces. --- python/vyos/vpp/control_host.py | 12 ++++++++++++ src/conf_mode/vpp.py | 2 ++ 2 files changed, 14 insertions(+) (limited to 'python') diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py index ead9f6968..1ac004fbb 100644 --- a/python/vyos/vpp/control_host.py +++ b/python/vyos/vpp/control_host.py @@ -21,6 +21,8 @@ from re import fullmatch as re_fullmatch from subprocess import run from time import sleep +from pyroute2 import IPRoute + from vyos.vpp.utils import EthtoolGDrvinfo @@ -361,3 +363,13 @@ def set_status(iface_name: str, status: str) -> None: status (str): status - "up" or "down" """ run(['ip', 'link', 'set', iface_name, status]) + + +def flush_ip(iface_name: str) -> None: + """Flush IP addresses from an interface + + Args: + iface_name (str): name of an interface + """ + iproute = IPRoute() + iproute.flush_addr(label=iface_name) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 76aa7c5af..d5778c778 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -693,6 +693,7 @@ def apply(config): if 'promisc' in iface_config['xdp_options']: control_host.set_promisc(f'defunct_{iface}', 'on') control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') # Rename Mellanox interfaces to hide them and create LCP properly if ( iface in Section.interfaces() @@ -700,6 +701,7 @@ def apply(config): ): control_host.rename_iface(iface, f'defunct_{iface}') control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') # Create lcp if iface not in Section.interfaces(): vpp_control.lcp_pair_add(iface, iface) -- cgit v1.2.3 From 6eb93578a33f27007c4ecda8b71efbd880a97653 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:36:18 +0300 Subject: T7424: Refactor resource validation and broaden cases (#38) * T7424: Refactor and extend resource usage verification on commit for VPP CLI T7424: Fix ruff errors * T7424: Implement check for smoke tests runtime; reduce resource requirements for test environments T7424: Fix errors in calculating the skipped and reserved CPU cores; Adjust default main heap size value. * T7424: Refactor the CPU checks logic; Add total CPU usage check T7424: Fix CPU reserve and skip cores calculations; Add total CPU usage check T7424: Refactor smoketests to reflect new logic * T7424: Refactor the CPU and memory checks logic --------- Co-authored-by: oniko94 --- .../include/vpp_host_resources.xml.i | 4 +- interface-definitions/vpp.xml.in | 7 +- python/vyos/vpp/config_resource_checks/__init__.py | 0 python/vyos/vpp/config_resource_checks/cpu.py | 76 ++++++ python/vyos/vpp/config_resource_checks/memory.py | 152 ++++++++++++ .../config_resource_checks/resource_defaults.py | 62 +++++ python/vyos/vpp/config_verify.py | 228 +++++++++++++++++ smoketest/scripts/cli/test_vpp.py | 28 ++- src/conf_mode/vpp.py | 271 +++++++-------------- 9 files changed, 639 insertions(+), 189 deletions(-) create mode 100644 python/vyos/vpp/config_resource_checks/__init__.py create mode 100644 python/vyos/vpp/config_resource_checks/cpu.py create mode 100644 python/vyos/vpp/config_resource_checks/memory.py create mode 100644 python/vyos/vpp/config_resource_checks/resource_defaults.py (limited to 'python') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 7f988ce44..109982bef 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -15,7 +15,7 @@ - 1024 + 2048 @@ -28,7 +28,7 @@ - 3096 + 4096 diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index dab0ea308..fa66d6126 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -427,11 +427,11 @@ Skip cores - u32:0-512 + u32:1-512 Skip cores - + @@ -771,12 +771,14 @@ Main heap size #include + 3G Main heap page size #include + 2M @@ -785,6 +787,7 @@ + 2M diff --git a/python/vyos/vpp/config_resource_checks/__init__.py b/python/vyos/vpp/config_resource_checks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py new file mode 100644 index 000000000..6f360ae74 --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/cpu.py @@ -0,0 +1,76 @@ +# Used for validating estimated CPU/physical cores use +# +# Copyright (C) 2025 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.utils.cpu import get_available_cpus, get_core_count + +from vyos.vpp.config_resource_checks.resource_defaults import get_resource_defaults + + +# Get default value for reserved cpu cores +reserved_cpus = get_resource_defaults().get('reserved_cpu_cores') + + +def available_cores_count(cpu_settings: dict) -> int: + core_count = get_core_count() + + if cpu_settings.get('main_core'): + core_count -= 1 + + skip_cores = int(cpu_settings.get('skip_cores', 0)) + # The default settings assume that + # at least 2 CPU cores should remain reserved for system use + # (only in case of current runtime is not smoke test) + if skip_cores < reserved_cpus: + core_count -= reserved_cpus + else: + core_count -= skip_cores + + return core_count + + +def available_cores_list(skip_cores: int) -> list: + # Available cores are all CPU cores without first N skipped cores that will not be used + # Get all available physical cores - use set to filter out unique values + cpu_cores = set(map(lambda el: el['cpu'], get_available_cpus())) + cpu_cores = list(cpu_cores) + + return cpu_cores[skip_cores:] + + +def worker_cores_list(iface: str, worker_ranges: list) -> list: + all_core_numbers = [] + for worker_range in worker_ranges: + core_numbers = worker_range.split('-') + + if int(core_numbers[0]) > int(core_numbers[-1]): + raise ValueError( + f'Range for "{iface} workers {worker_range}" is not correct' + ) + + all_core_numbers.extend(range(int(core_numbers[0]), int(core_numbers[-1]) + 1)) + + # Check for duplicates + duplicates = set( + [str(x) for n, x in enumerate(all_core_numbers) if x in all_core_numbers[:n]] + ) + if duplicates: + raise ValueError( + f'Some workers in "{iface} workers" are duplicated: #{",".join(list(duplicates))}' + ) + + return all_core_numbers diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py new file mode 100644 index 000000000..44f0c9358 --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -0,0 +1,152 @@ +# Used for memory consumption calculations +# +# Copyright (C) 2025 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 re + +from vyos.utils.process import cmd +from vyos.vpp.utils import ( + human_memory_to_bytes, + human_page_memory_to_bytes, +) +from vyos.vpp.config_resource_checks.resource_defaults import get_resource_defaults + + +# Get default values for resource checks +defaults = get_resource_defaults() + + +def get_total_hugepages_free_memory() -> int: + """ + Returns the total amount of hugepage-backed free memory (in bytes) + as reported by /proc/meminfo + """ + info = {} + with open('/proc/meminfo', 'r') as meminfo: + for line in meminfo: + if line.startswith('Huge'): + key, value, *_ = line.strip().split() + info[key.rstrip(':')] = int(value) + + hugepages_free = info.get('HugePages_Free') + hugepage_size = info.get('Hugepagesize') * 1024 + + return hugepage_size * hugepages_free + + +def get_numa_count(): + """ + Run `numactl --hardware` and parse the 'available:' line. + """ + out = cmd('numactl --hardware') + # e.g. "available: 2 nodes (0-1)" + m = re.search(r'available:\s*(\d+)\s+nodes', out) + return int(m.group(1)) if m else 0 + + +def get_memory_from_kernel_settings(settings: dict) -> int: + hugepage_settings = settings.get('hugepage_size', {}) + + total_bytes = 0 + for size_str, info in hugepage_settings.items(): + count = int(info.get('hugepage_count', 0)) + page_bytes = human_memory_to_bytes(size_str) + total_bytes += count * page_bytes + + return total_bytes + + +def buffer_size(settings: dict) -> int: + numa_count = get_numa_count() + buffers_per_numa = int( + settings.get('buffers', {}).get( + 'buffers_per_numa', defaults.get('buffers_per_numa') + ) + ) + data_size = int( + settings.get('buffers', {}).get('data_size', defaults.get('data_size')) + ) + buffers_memory = buffers_per_numa * data_size * numa_count + return buffers_memory + + +def main_heap_page_size(settings: dict) -> int: + heap_page_size = settings.get('memory', {}).get( + 'main_heap_page_size', defaults.get('main_heap_page_size') + ) + return human_page_memory_to_bytes(heap_page_size) + + +def memory_main_heap(settings: dict) -> int: + heap_size = settings.get('memory', {}).get( + 'main_heap_size', defaults.get('main_heap_size') + ) + return human_memory_to_bytes(heap_size) + + +def ipv6_heap_size(settings: dict) -> int: + heap_size = settings.get('ipv6', {}).get( + 'heap_size', defaults.get('ipv6_heap_size') + ) + return human_memory_to_bytes(heap_size) + + +def total_heap_size(heap_size: int, heap_page_size: int) -> int: + return (heap_size + heap_page_size - 1) & ~(heap_page_size - 1) + + +def statseg_size(settings: dict) -> int: + statseg_memory = settings.get('statseg', {}).get( + 'size', defaults.get('statseg_heap_size') + ) + return human_memory_to_bytes(statseg_memory) + + +def statseg_page_size(settings: dict) -> int: + page_size = settings.get('statseg', {}).get('page_size', 'default') + return human_page_memory_to_bytes(page_size) + + +def total_statseg_size(_statseg_size: int, _statseg_page: int) -> int: + return (_statseg_size + _statseg_page - 1) & ~(_statseg_page - 1) + + +def total_memory_required(settings: dict) -> int: + mem_required = 0 + + mem_stats = { + 'memory_buffers': buffer_size(settings), + 'netlink_buffer_size': int( + settings.get('lcp', {}).get( + 'rx_buffer_size', defaults.get('netlink_rx_buffer_size') + ) + ), + 'heap_size': total_heap_size( + heap_size=memory_main_heap(settings), + heap_page_size=main_heap_page_size(settings), + ), + 'statseg_size': total_statseg_size( + _statseg_size=statseg_size(settings), + _statseg_page=statseg_page_size(settings), + ), + 'ipv6_heap_size': ipv6_heap_size(settings), + } + + for stat in mem_stats: + mem_required += mem_stats[stat] + + return mem_required diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py new file mode 100644 index 000000000..3c618accd --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py @@ -0,0 +1,62 @@ +# Default values for resource consumption checks +# +# Copyright (C) 2025 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 copy +import os + + +__default_resource_map = { + # Default amount of buffers per NUMA (populated CPU socket) + 'buffers_per_numa': 16384, + # Default size of buffer (in bytes) + 'data_size': 2048, + # Default hugepage size for VPP + 'hugepage_size': '2M', + # Default amount of memory allocated for VPP exclusive usage + 'main_heap_size': '3G', + # Default main heap page size + 'main_heap_page_size': '2M', + # Default size of buffers transferred via netlink + 'netlink_rx_buffer_size': 212992, + # Default amount of memory allocated for VPP stats segment usage + 'statseg_heap_size': '96M', + # Minimal amount of memory required to start VPP + 'min_memory': '8G', + # Minimal number of physical CPU cores required to start VPP + 'min_cpus': 4, + # Reserve at least 2 physical cores + 'reserved_cpu_cores': 2, + # Default heap size for IPv6 + 'ipv6_heap_size': '32M', +} + + +def get_resource_defaults() -> dict: + resource_map = copy.deepcopy(__default_resource_map) + # Check if current runtime is smoke tests + # Since CI/CD runners are limited in resources, reduce the checks for tests + if is_smoketest(): + resource_map.update(min_memory='6G', min_cpus=2, reserved_cpu_cores=0) + + return resource_map + + +def is_smoketest(): + if os.path.exists('/tmp/vyos.smoketests.hint'): + return True + return False diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 830d4af73..9cfbfb7cd 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -16,9 +16,19 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import psutil + from vyos import ConfigError +from vyos.utils.cpu import get_core_count as total_core_count from vyos.vpp.control_host import get_eth_driver +from vyos.vpp.config_resource_checks import cpu as cpu_checks, memory as mem_checks +from vyos.vpp.config_resource_checks import resource_defaults +from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory + + +# Get default values for resource checks +defaults = resource_defaults.get_resource_defaults() def verify_vpp_remove_kernel_interface(config: dict): @@ -171,3 +181,221 @@ def verify_dev_driver(iface_name: str, driver_type: str) -> bool: raise ConfigError(f'"Driver type {driver_type} is wrong') return False + + +def verify_vpp_minimum_cpus(): + """ + Verify that the host system has enough physical CPU cores + Current minimal requirement is 4 + """ + min_cpus = defaults.get('min_cpus') + if total_core_count() < min_cpus: + raise ConfigError( + 'This system does not meet minimal requirements for VPP. ' + f'Minimum {min_cpus} CPU cores are required.' + ) + + +def verify_vpp_minimum_memory(): + """ + Verify that the host system has enough RAM + Calculate by retrieving the amount of physical memory + And the minimal requirement (currently 8 GB). Round before comparing - + To avoid situations like when a machine nominally has 8192 MB (8 giga/gibibytes) + But the OS sees only 7.75 GB, creating a fail condition for this check + """ + min_mem = defaults.get('min_memory') + total_memory = round(psutil.virtual_memory().total / (1024**3)) + min_memory = round(human_memory_to_bytes(min_mem) / (1024**3)) + + if total_memory < min_memory: + raise ConfigError( + 'This system does not meet minimal requirements for VPP. ' + f'Minimum {min_memory} GB of RAM are required.' + ) + + +def verify_vpp_memory(config: dict): + main_heap_size = mem_checks.memory_main_heap(config['settings']) + main_heap_page_size = mem_checks.main_heap_page_size(config['settings']) + + if main_heap_size < 51 << 20: + raise ConfigError('The main heap size must be greater than or equal to 51M') + + readable_heap_page = bytes_to_human_memory(main_heap_page_size, 'K') + + if main_heap_page_size > main_heap_size: + raise ConfigError( + f'The main heap size must be greater than or equal to page-size ({readable_heap_page})' + ) + + # Get available HupePage memory to compare with required memory for VPP + # (if it's smketests environment get system kernel settings for HugePages) + if not resource_defaults.is_smoketest(): + available_memory = mem_checks.get_total_hugepages_free_memory() + else: + available_memory = mem_checks.get_memory_from_kernel_settings( + config['kernel_memory_settings'] + ) + + memory_required = mem_checks.total_memory_required(config['settings']) + + # Check if there is a config currently active + # If yes, calculate how much memory it consumes + # and exclude it from required memory + if config.get('effective'): + memory_used = mem_checks.total_memory_required(config['effective']['settings']) + # If we want to reduce memory configs then there is nothing to check + if memory_used > memory_required: + return + memory_required -= memory_used + + if memory_required > available_memory: + raise ConfigError( + 'Not enough free memory to start VPP: ' + f'available: {round(available_memory / 1024 ** 3, 1)} GB, ' + f'required: {round(memory_required / 1024 ** 3, 1)} GB. ' + 'Please add kernel memory options for HugePages and reboot' + ) + + +def verify_vpp_settings_cpu_skip_cores(skip_cores: int): + cpu_cores = total_core_count() + + # The number of skipped cores must not be greater than + # available CPU cores in the system - 1 for main thread + if skip_cores > (cpu_cores - 1): + raise ConfigError( + f'The system does not have enough available CPUs to skip ' + f'(reduce "cpu skip-cores" to {cpu_cores} or less)' + ) + + +def verify_vpp_settings_cpu_and_corelist_workers(settings: dict): + """ + `set vpp settings cpu workers` and `set vpp settings cpu corelist-workers` + are mutually exclusive! + """ + if ( + 'corelist_workers' in settings or 'workers' in settings + ) and 'main_core' not in settings: + raise ConfigError('"cpu main-core" is required but not set!') + + if 'corelist_workers' in settings and 'workers' in settings: + raise ConfigError( + '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!' + ) + + +def verify_vpp_cpu_main_core(cpu_settings: dict) -> None: + """Check that the main core is available""" + skip_cores = int(cpu_settings.get('skip_cores', 0)) + available_cores = cpu_checks.available_cores_list(skip_cores) + main_core = int(cpu_settings['main_core']) + + if main_core not in available_cores: + raise ConfigError( + 'Cannot set main core for VPP process: ' + f'CPU#{main_core} is not available.' + ) + + +def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int: + """ + Verify that the system has enough available CPU cores + to run a given amount of worker processes (1 worker/core) + """ + workers = int(cpu_settings.get('workers', 0)) + available_cores = cpu_checks.available_cores_count(cpu_settings) + + if workers > available_cores: + raise ConfigError( + f'Not enough free CPU cores for {workers} VPP workers ' + f'(reduce to {available_cores} or less)' + ) + + return workers + + +def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int: + """ + Verify that the CPU cores provided to the config are free and can be used by VPP + """ + workers = cpu_settings.get('corelist_workers') + main_core = int(cpu_settings.get('main_core')) + skip_cores = int(cpu_settings.get('skip_cores', 0)) + available_cores = cpu_checks.available_cores_list(skip_cores) + try: + all_core_nums = cpu_checks.worker_cores_list( + iface='cpu corelist', worker_ranges=workers + ) + except ValueError as e: + raise ConfigError(str(e)) + + error_msg = 'Cannot set VPP "cpu corelist-workers"' + + if main_core in all_core_nums: + raise ConfigError( + f'CPU#{main_core} is set as main core and should not ' + 'be included to the corelist-workers' + ) + + invalid_cores = [str(el) for el in all_core_nums if el not in available_cores] + if invalid_cores: + raise ConfigError( + f'{error_msg}: CPU# {",".join(invalid_cores)} are not available.' + ) + + if len(all_core_nums) > cpu_checks.available_cores_count(cpu_settings): + raise ConfigError(f'{error_msg}: Not enough free CPUs in the system.') + + return len(all_core_nums) + + +def verify_vpp_nat44_workers(workers: int, nat44_workers: list): + if workers < 1: + raise ConfigError( + '"nat44 workers" requires cpu workers or corelist-workers to be set!' + ) + try: + nat_workers = cpu_checks.worker_cores_list( + iface='nat44', worker_ranges=nat44_workers + ) + except ValueError as e: + raise ConfigError(str(e)) + + invalid_workers = [str(el) for el in nat_workers if el not in range(workers)] + if invalid_workers: + raise ConfigError( + f'Cannot set VPP "nat44 workers": worker(s) #{",".join(invalid_workers)} not available. ' + f'Available worker ids: {",".join(map(str, range(workers)))}' + ) + + +def verify_vpp_statseg_size(settings: dict): + statseg_size = mem_checks.statseg_size(settings) + + if 'size' in settings.get('statseg'): + if statseg_size < 1 << 20: + raise ConfigError('The statseg size must be greater than or equal to 1M') + + if 'page_size' in settings['statseg']: + statseg_page_size = mem_checks.statseg_page_size(settings) + if statseg_page_size > statseg_size: + readable_statseg_page = bytes_to_human_memory(statseg_page_size, 'K') + raise ConfigError( + f'The statseg size must be greater than or equal to page-size ({readable_statseg_page})' + ) + + +def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: int): + """ + Verify that VPP has enough workers to run the given amount of RX/TX queues + 1 queue per 1 worker is assumed as default + """ + + if num_queues > workers: + raise ConfigError( + f'The number of {qtype} queues cannot be greater than the number of configured VPP workers: ' + f'workers: {workers}, queues: {num_queues}' + ) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index d8f7f3473..3dfbb4cc8 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -41,6 +41,7 @@ VPP_CONF = '/run/vpp/vpp.conf' base_path = ['vpp'] driver = 'dpdk' interface = 'eth1' +system_memory_path = ['system', 'option', 'kernel', 'memory'] def get_vpp_config(): @@ -96,6 +97,9 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def setUp(self): self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver]) self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10']) + self.cli_set( + system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] + ) def tearDown(self): try: @@ -110,6 +114,12 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'ethernet', interface, 'address']) self.cli_commit() + # delete kernel memory settings + self.cli_delete( + system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] + ) + self.cli_commit() + self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) @@ -1084,15 +1094,25 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): dpdk_options = { 'num-rx-desc': '512', 'num-tx-desc': '512', - 'num-rx-queues': '3', - 'num-tx-queues': '3', + 'num-rx-queues': '1', + 'num-tx-queues': '1', } + main_core = '0' + workers = '1' base_interface_path = base_path + ['settings', 'interface', interface] for option, value in dpdk_options.items(): self.cli_set(base_interface_path + ['dpdk-options', option, value]) + # rx/tx queue configuration expect VPP workers to be set + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core]) + self.cli_set(base_path + ['settings', 'cpu', 'workers', workers]) + # DPDK driver expect only dpdk-options and not xdp-options to be set # expect raise ConfigError self.cli_set(base_interface_path + ['xdp-options', 'no-syscall-lock']) @@ -1112,7 +1132,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def test_11_vpp_cpu_settings(self): main_core = '2' - workers = '2' + workers = '1' skip_cores = '1' self.cli_set(base_path + ['settings', 'cpu', 'workers', workers]) @@ -1149,7 +1169,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def test_12_vpp_cpu_corelist_workers(self): main_core = '0' - corelist_workers = ['1', '2-3'] + corelist_workers = ['3'] for worker in corelist_workers: self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', worker]) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index d5778c778..757904dd5 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -18,7 +18,6 @@ from pathlib import Path -from psutil import virtual_memory from pyroute2 import IPRoute from vpp_papi import VPPIOError, VPPValueError @@ -28,7 +27,6 @@ from vyos.base import Warning from vyos.config import Config, config_dict_merge 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, get_available_cpus from vyos.ifconfig import Section from vyos.template import render from vyos.utils.boot import boot_configuration_complete @@ -38,14 +36,22 @@ 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, - human_page_memory_to_bytes, - human_memory_to_bytes, - bytes_to_human_memory, +from vyos.vpp.config_verify import ( + verify_dev_driver, + verify_vpp_minimum_cpus, + verify_vpp_minimum_memory, + verify_vpp_settings_cpu_and_corelist_workers, + verify_vpp_settings_cpu_corelist_workers, + verify_vpp_cpu_main_core, + verify_vpp_settings_cpu_skip_cores, + verify_vpp_settings_cpu_workers, + verify_vpp_nat44_workers, + verify_vpp_memory, + verify_vpp_statseg_size, + verify_vpp_interfaces_dpdk_num_queues, ) +from vyos.vpp.config_filter import iface_filter_eth +from vyos.vpp.utils import EthtoolGDrvinfo from vyos.vpp.configdb import JSONStorage airbag.enable() @@ -164,7 +170,7 @@ def get_config(config=None): # dictionary retrieved. default_values = conf.get_config_defaults(**config.kwargs, recursive=True) - # delete "xdp-options" from defaults if driver is DPDK + # delete 'xdp-options' from defaults if driver is DPDK for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): if iface_config.get('driver') == 'dpdk': del default_values['settings']['interface'][iface]['xdp_options'] @@ -278,63 +284,21 @@ def get_config(config=None): eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + # Get kernel settings for hugepages + kernel_memory_settings = conf.get_config_dict( + ['system', 'option', 'kernel', 'memory'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + config['kernel_memory_settings'] = kernel_memory_settings + # Return to config dictionary config['persist_config'] = eth_ifaces_persist return config -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 = human_memory_to_bytes( - settings.get('memory', {}).get('main_heap_size', '1G') - ) - - if memory_main_heap < 51 << 20: - # vpp is aborted when we try to use a smaller heap size - raise ConfigError('The main heap size must be greater than or equal to 51M') - - memory_main_heap_page_size = human_page_memory_to_bytes( - settings.get('memory', {}).get('main_heap_page_size', 'default') - ) - - if memory_main_heap_page_size > memory_main_heap: - raise ConfigError( - f'The main heap size must be greater than or equal to page-size({bytes_to_human_memory(memory_main_heap_page_size, "K")})' - ) - - memory_required += (memory_main_heap + memory_main_heap_page_size - 1) & ~( - memory_main_heap_page_size - 1 - ) - - statseg_size = human_memory_to_bytes(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): @@ -346,12 +310,64 @@ def verify(config): if 'interface' not in config['settings']: raise ConfigError('"settings interface" is required but not set!') + # check if the system meets minimal requirements + verify_vpp_minimum_cpus() + verify_vpp_minimum_memory() + # 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!') + # Resource usage checks + workers = 0 + + if 'cpu' in config['settings']: + cpu_settings = config['settings']['cpu'] + + # Check if there are enough CPU cores to skip according to config + if 'skip_cores' in cpu_settings: + skip_cores = int(cpu_settings['skip_cores']) + verify_vpp_settings_cpu_skip_cores(skip_cores) + + # Check whether the workers and corelist-workers are configured properly + verify_vpp_settings_cpu_and_corelist_workers(cpu_settings) + + # Check if there are enough CPU cores to add workers + if 'workers' in cpu_settings: + workers = verify_vpp_settings_cpu_workers(cpu_settings) + + if 'main_core' in cpu_settings: + verify_vpp_cpu_main_core(cpu_settings) + + # Check the CPU main core not falling to the corelist-workers + if 'corelist_workers' in cpu_settings: + workers = verify_vpp_settings_cpu_corelist_workers(cpu_settings) + + if 'workers' in config['settings']['nat44']: + verify_vpp_nat44_workers( + workers=workers, nat44_workers=config['settings']['nat44']['workers'] + ) + + # Check if available memory is enough for current VPP config + verify_vpp_memory(config) + + if 'host_resources' in config['settings']: + if ( + 'nr_hugepages' in config['settings']['host_resources'] + and 'max_map_count' in config['settings']['host_resources'] + ): + if int(config['settings']['host_resources']['max_map_count']) < 2 * int( + config['settings']['host_resources']['nr_hugepages'] + ): + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' + ) + + if 'statseg' in config['settings']: + verify_vpp_statseg_size(config['settings']) + # 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 @@ -372,6 +388,19 @@ def verify(config): if iface_config['driver'] == 'dpdk' and 'xdp_options' in iface_config: raise ConfigError('XDP options are not applicable for DPDK driver!') + if iface_config['driver'] == 'dpdk' and 'dpdk_options' in iface_config: + if 'num_rx_queues' in iface_config['dpdk_options']: + rx_queues = int(iface_config['dpdk_options']['num_rx_queues']) + verify_vpp_interfaces_dpdk_num_queues( + qtype='receive', num_queues=rx_queues, workers=workers + ) + + if 'num_tx_queues' in iface_config['dpdk_options']: + tx_queues = int(iface_config['dpdk_options']['num_tx_queues']) + verify_vpp_interfaces_dpdk_num_queues( + qtype='transmit', num_queues=tx_queues, workers=workers + ) + # RX-mode verification rx_mode = iface_config.get('rx_mode') if rx_mode and rx_mode != 'polling': @@ -429,126 +458,6 @@ def verify(config): 'Only one multipoint GRE tunnel is allowed from the same source address' ) - workers = 0 - if 'cpu' in config['settings']: - if ( - 'corelist_workers' in config['settings']['cpu'] - or 'workers' in config['settings']['cpu'] - ) and 'main_core' not in config['settings']['cpu']: - raise ConfigError('"cpu main-core" is required but not set!') - - if ( - 'corelist_workers' in config['settings']['cpu'] - and 'workers' in config['settings']['cpu'] - ): - raise ConfigError( - '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!' - ) - - cpus = int(get_core_count()) - skip_cores = 0 - - if 'skip_cores' in config['settings']['cpu']: - skip_cores = int(config['settings']['cpu']['skip_cores']) - # the number of skipped cores should not be more than all CPUs - 1 (for main core) - if skip_cores > cpus - 1: - raise ConfigError( - f'The system does not have enough available CPUs to skip ' - f'(reduce "cpu skip-cores" to {cpus - 1} or less)' - ) - - if 'workers' in config['settings']['cpu']: - # number of worker threads must be not more than - # available CPUs in the system - 1 for main thread - number of skipped cores - # or - 1 (at least) for system processes - workers = int(config['settings']['cpu']['workers']) - available_workers = cpus - 1 - (skip_cores or 1) - if workers > available_workers: - raise ConfigError( - f'The system does not have enough CPUs for {workers} VPP workers ' - f'(reduce to {available_workers} or less)' - ) - - cpus_available = list(map(lambda el: el['cpu'], get_available_cpus())) - # available CPUs are all CPUs without first N skipped cores that will not be used - cpus_available = cpus_available[skip_cores:] - - if 'main_core' in config['settings']['cpu']: - main_core = int(config['settings']['cpu']['main_core']) - - if main_core not in cpus_available: - raise ConfigError(f'"cpu main-core {main_core}" is not available!') - - # CPU main-core must be not included to corelist-workers - if config.get('settings').get('cpu', {}).get('corelist_workers'): - corelist_workers = config['settings']['cpu']['corelist_workers'] - - all_core_numbers = [] - for worker_range in corelist_workers: - core_numbers = worker_range.split('-') - if int(core_numbers[0]) > int(core_numbers[-1]): - raise ConfigError( - f'Range for "cpu corelist-workers {worker_range}" is not correct' - ) - 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 not all(el in cpus_available for el in all_core_numbers): - raise ConfigError('"cpu corelist-workers" is not correct') - - workers = len(all_core_numbers) - - if 'workers' in config['settings']['nat44']: - nat_workers = [] - for worker_range in config['settings']['nat44']['workers']: - worker_numbers = worker_range.split('-') - if int(worker_numbers[0]) > int(worker_numbers[-1]): - raise ConfigError( - f'Range for "nat44 workers {worker_range}" is not correct' - ) - nat_workers.extend( - range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1) - ) - if not all(el in list(range(workers)) for el in nat_workers): - raise ConfigError('"nat44 workers" is not correct') - - verify_memory(config['settings']) - if 'host_resources' in config['settings']: - if ( - 'nr_hugepages' in config['settings']['host_resources'] - and 'max_map_count' in config['settings']['host_resources'] - ): - if int(config['settings']['host_resources']['max_map_count']) < 2 * int( - config['settings']['host_resources']['nr_hugepages'] - ): - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' - ) - - if 'statseg' in config['settings']: - _size = human_memory_to_bytes(config['settings']['statseg'].get('size', '96M')) - - if 'size' in config['settings']['statseg']: - if _size < 1 << 20: - raise ConfigError( - 'The statseg size must be greater than or equal to 1M' - ) - - if 'page_size' in config['settings']['statseg']: - _page_size = human_page_memory_to_bytes( - config['settings']['statseg']['page_size'] - ) - if _page_size > _size: - raise ConfigError( - f'The statseg size must be greater than or equal to page-size({bytes_to_human_memory(_page_size, "K")})' - ) - # 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', {}): -- cgit v1.2.3 From 1c94f3a6a6b92b629ca5a886c92ee6463e85e121 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Wed, 25 Jun 2025 14:05:34 +0300 Subject: T7424: Add missing hugepage memory configuration for config load tests --- python/vyos/vpp/config_verify.py | 2 +- smoketest/config-tests/vpp | 1 + smoketest/configs/vpp | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 9cfbfb7cd..4505d205f 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -252,7 +252,7 @@ def verify_vpp_memory(config: dict): if memory_required > available_memory: raise ConfigError( - 'Not enough free memory to start VPP: ' + 'Not enough free hugepage memory to start VPP: ' f'available: {round(available_memory / 1024 ** 3, 1)} GB, ' f'required: {round(memory_required / 1024 ** 3, 1)} GB. ' 'Please add kernel memory options for HugePages and reboot' diff --git a/smoketest/config-tests/vpp b/smoketest/config-tests/vpp index 50d13990c..e42568965 100644 --- a/smoketest/config-tests/vpp +++ b/smoketest/config-tests/vpp @@ -15,6 +15,7 @@ set system console device ttyS0 speed '115200' set system host-name 'r16' set system login user vyos authentication encrypted-password '$6$rounds=656000$FZBlGpWsmSrV2Rvq$YPNAPtk4k6u99FAMxR6cw4DUPCgOomwCgRZRSO5rAoJK8RlMSCkVAFVF3ozL/3mZMfxcuCnwvd5HX6f9V5KzO.' set system name-server '203.0.113.1' +set system option kernel memory hugepage-size 2M hugepage-count '2048' set system option time-format '24-hour' set system syslog local facility all level 'info' set system syslog local facility local7 level 'debug' diff --git a/smoketest/configs/vpp b/smoketest/configs/vpp index e2223bc02..472c2919b 100644 --- a/smoketest/configs/vpp +++ b/smoketest/configs/vpp @@ -42,6 +42,13 @@ system { } name-server "203.0.113.1" option { + kernel { + memory { + hugepage-size 2M { + hugepage-count "2048" + } + } + } time-format "24-hour" } syslog { -- cgit v1.2.3 From fce4f84c69e3cb6e130fbe486154134bd62cd5f6 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:43:21 +0200 Subject: T7611: VPP Rewrite check for CPUs and memory (#44) --- python/vyos/vpp/config_resource_checks/cpu.py | 4 ++-- python/vyos/vpp/config_resource_checks/memory.py | 22 ++++++++++------------ .../config_resource_checks/resource_defaults.py | 21 +-------------------- python/vyos/vpp/config_verify.py | 20 +++++--------------- smoketest/scripts/cli/test_vpp.py | 10 ---------- 5 files changed, 18 insertions(+), 59 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py index 6f360ae74..ee0bace2d 100644 --- a/python/vyos/vpp/config_resource_checks/cpu.py +++ b/python/vyos/vpp/config_resource_checks/cpu.py @@ -18,11 +18,11 @@ from vyos.utils.cpu import get_available_cpus, get_core_count -from vyos.vpp.config_resource_checks.resource_defaults import get_resource_defaults +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map # Get default value for reserved cpu cores -reserved_cpus = get_resource_defaults().get('reserved_cpu_cores') +reserved_cpus = default_resource_map.get('reserved_cpu_cores') def available_cores_count(cpu_settings: dict) -> int: diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index 44f0c9358..f5c7286bc 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -23,11 +23,7 @@ from vyos.vpp.utils import ( human_memory_to_bytes, human_page_memory_to_bytes, ) -from vyos.vpp.config_resource_checks.resource_defaults import get_resource_defaults - - -# Get default values for resource checks -defaults = get_resource_defaults() +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map def get_total_hugepages_free_memory() -> int: @@ -74,11 +70,13 @@ def buffer_size(settings: dict) -> int: numa_count = get_numa_count() buffers_per_numa = int( settings.get('buffers', {}).get( - 'buffers_per_numa', defaults.get('buffers_per_numa') + 'buffers_per_numa', default_resource_map.get('buffers_per_numa') ) ) data_size = int( - settings.get('buffers', {}).get('data_size', defaults.get('data_size')) + settings.get('buffers', {}).get( + 'data_size', default_resource_map.get('data_size') + ) ) buffers_memory = buffers_per_numa * data_size * numa_count return buffers_memory @@ -86,21 +84,21 @@ def buffer_size(settings: dict) -> int: def main_heap_page_size(settings: dict) -> int: heap_page_size = settings.get('memory', {}).get( - 'main_heap_page_size', defaults.get('main_heap_page_size') + 'main_heap_page_size', default_resource_map.get('main_heap_page_size') ) return human_page_memory_to_bytes(heap_page_size) def memory_main_heap(settings: dict) -> int: heap_size = settings.get('memory', {}).get( - 'main_heap_size', defaults.get('main_heap_size') + 'main_heap_size', default_resource_map.get('main_heap_size') ) return human_memory_to_bytes(heap_size) def ipv6_heap_size(settings: dict) -> int: heap_size = settings.get('ipv6', {}).get( - 'heap_size', defaults.get('ipv6_heap_size') + 'heap_size', default_resource_map.get('ipv6_heap_size') ) return human_memory_to_bytes(heap_size) @@ -111,7 +109,7 @@ def total_heap_size(heap_size: int, heap_page_size: int) -> int: def statseg_size(settings: dict) -> int: statseg_memory = settings.get('statseg', {}).get( - 'size', defaults.get('statseg_heap_size') + 'size', default_resource_map.get('statseg_heap_size') ) return human_memory_to_bytes(statseg_memory) @@ -132,7 +130,7 @@ def total_memory_required(settings: dict) -> int: 'memory_buffers': buffer_size(settings), 'netlink_buffer_size': int( settings.get('lcp', {}).get( - 'rx_buffer_size', defaults.get('netlink_rx_buffer_size') + 'rx_buffer_size', default_resource_map.get('netlink_rx_buffer_size') ) ), 'heap_size': total_heap_size( diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py index 3c618accd..828683456 100644 --- a/python/vyos/vpp/config_resource_checks/resource_defaults.py +++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py @@ -16,11 +16,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import copy -import os - -__default_resource_map = { +default_resource_map = { # Default amount of buffers per NUMA (populated CPU socket) 'buffers_per_numa': 16384, # Default size of buffer (in bytes) @@ -44,19 +41,3 @@ __default_resource_map = { # Default heap size for IPv6 'ipv6_heap_size': '32M', } - - -def get_resource_defaults() -> dict: - resource_map = copy.deepcopy(__default_resource_map) - # Check if current runtime is smoke tests - # Since CI/CD runners are limited in resources, reduce the checks for tests - if is_smoketest(): - resource_map.update(min_memory='6G', min_cpus=2, reserved_cpu_cores=0) - - return resource_map - - -def is_smoketest(): - if os.path.exists('/tmp/vyos.smoketests.hint'): - return True - return False diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 4505d205f..cda5cf012 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -23,14 +23,10 @@ from vyos.utils.cpu import get_core_count as total_core_count from vyos.vpp.control_host import get_eth_driver from vyos.vpp.config_resource_checks import cpu as cpu_checks, memory as mem_checks -from vyos.vpp.config_resource_checks import resource_defaults +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory -# Get default values for resource checks -defaults = resource_defaults.get_resource_defaults() - - 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'. @@ -188,7 +184,7 @@ def verify_vpp_minimum_cpus(): Verify that the host system has enough physical CPU cores Current minimal requirement is 4 """ - min_cpus = defaults.get('min_cpus') + min_cpus = default_resource_map.get('min_cpus') if total_core_count() < min_cpus: raise ConfigError( 'This system does not meet minimal requirements for VPP. ' @@ -204,7 +200,7 @@ def verify_vpp_minimum_memory(): To avoid situations like when a machine nominally has 8192 MB (8 giga/gibibytes) But the OS sees only 7.75 GB, creating a fail condition for this check """ - min_mem = defaults.get('min_memory') + min_mem = default_resource_map.get('min_memory') total_memory = round(psutil.virtual_memory().total / (1024**3)) min_memory = round(human_memory_to_bytes(min_mem) / (1024**3)) @@ -229,14 +225,8 @@ def verify_vpp_memory(config: dict): f'The main heap size must be greater than or equal to page-size ({readable_heap_page})' ) - # Get available HupePage memory to compare with required memory for VPP - # (if it's smketests environment get system kernel settings for HugePages) - if not resource_defaults.is_smoketest(): - available_memory = mem_checks.get_total_hugepages_free_memory() - else: - available_memory = mem_checks.get_memory_from_kernel_settings( - config['kernel_memory_settings'] - ) + # Get available HugePage memory to compare with required memory for VPP + available_memory = mem_checks.get_total_hugepages_free_memory() memory_required = mem_checks.total_memory_required(config['settings']) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 3dfbb4cc8..feb0f3fb3 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -41,7 +41,6 @@ VPP_CONF = '/run/vpp/vpp.conf' base_path = ['vpp'] driver = 'dpdk' interface = 'eth1' -system_memory_path = ['system', 'option', 'kernel', 'memory'] def get_vpp_config(): @@ -97,9 +96,6 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def setUp(self): self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver]) self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10']) - self.cli_set( - system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] - ) def tearDown(self): try: @@ -114,12 +110,6 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'ethernet', interface, 'address']) self.cli_commit() - # delete kernel memory settings - self.cli_delete( - system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] - ) - self.cli_commit() - self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) -- cgit v1.2.3 From 5d0f6aad265f2fa10c7d822e1d135072a6feaf4f Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:44:42 +0200 Subject: T7607: Remove "set vpp settings host-resources nr_hugepages " setting (#43) --- .../include/vpp_host_resources.xml.i | 13 ------------- python/vyos/vpp/config_resource_checks/memory.py | 21 ++++++++++++++++++--- python/vyos/vpp/config_verify.py | 12 ++++++++++++ src/conf_mode/vpp.py | 17 +++++------------ 4 files changed, 35 insertions(+), 28 deletions(-) (limited to 'python') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 109982bef..6fee0f2f3 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -4,19 +4,6 @@ Host resources control - - - Number of pre-allocated huge pages of the default size - - u32:0-4294967295 - Pages count - - - - - - 2048 - Maximum number of memory map areas a process may have diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index f5c7286bc..64fdf9abe 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -26,10 +26,10 @@ from vyos.vpp.utils import ( from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map -def get_total_hugepages_free_memory() -> int: +def get_hugepages_info() -> dict: """ - Returns the total amount of hugepage-backed free memory (in bytes) - as reported by /proc/meminfo + Returns the information about HugePages + retrieved from /proc/meminfo """ info = {} with open('/proc/meminfo', 'r') as meminfo: @@ -37,13 +37,28 @@ def get_total_hugepages_free_memory() -> int: if line.startswith('Huge'): key, value, *_ = line.strip().split() info[key.rstrip(':')] = int(value) + return info + +def get_total_hugepages_free_memory() -> int: + """ + Returns the total amount of hugepage-backed free memory (in bytes) + """ + info = get_hugepages_info() hugepages_free = info.get('HugePages_Free') hugepage_size = info.get('Hugepagesize') * 1024 return hugepage_size * hugepages_free +def get_hugepages_total() -> int: + """ + Returns the total count of hugepages + """ + info = get_hugepages_info() + return info.get('HugePages_Total') + + def get_numa_count(): """ Run `numactl --hardware` and parse the 'available:' line. diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index cda5cf012..65dfac91a 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -389,3 +389,15 @@ def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: f'The number of {qtype} queues cannot be greater than the number of configured VPP workers: ' f'workers: {workers}, queues: {num_queues}' ) + + +def verify_vpp_host_resources(config: dict): + max_map_count = int(config['settings']['host_resources']['max_map_count']) + + # Get HugePages total count + hugepages = mem_checks.get_hugepages_total() + + if max_map_count < 2 * hugepages: + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * HugePages_Total)' + ) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 757904dd5..9b9a3466b 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -49,6 +49,7 @@ from vyos.vpp.config_verify import ( verify_vpp_memory, verify_vpp_statseg_size, verify_vpp_interfaces_dpdk_num_queues, + verify_vpp_host_resources, ) from vyos.vpp.config_filter import iface_filter_eth from vyos.vpp.utils import EthtoolGDrvinfo @@ -353,17 +354,8 @@ def verify(config): # Check if available memory is enough for current VPP config verify_vpp_memory(config) - if 'host_resources' in config['settings']: - if ( - 'nr_hugepages' in config['settings']['host_resources'] - and 'max_map_count' in config['settings']['host_resources'] - ): - if int(config['settings']['host_resources']['max_map_count']) < 2 * int( - config['settings']['host_resources']['nr_hugepages'] - ): - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' - ) + if 'max_map_count' in config['settings'].get('host_resources', {}): + verify_vpp_host_resources(config) if 'statseg' in config['settings']: verify_vpp_statseg_size(config['settings']) @@ -477,8 +469,9 @@ def generate(config): # apply sysctl values # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf + # vm.nr_hugepages are now configured in section + # 'set system option kernel memory hugepage-size 2M hugepage-count ' 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'], -- cgit v1.2.3 From d37cca0672b71691614da0a0f770eaf0fe117fa4 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:33:11 +0200 Subject: T7658: T7656: Changes for max-map-count parameter (#53) * T7658: Increase max-map-count interval and default value to linux default Also input a warning instead of ConfigError in case of "host-resources max-map-count" didn't pass the verification * T7656: Clarify warning message for max-map-count option verification --- interface-definitions/include/vpp_host_resources.xml.i | 6 +++--- python/vyos/vpp/config_verify.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'python') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 6fee0f2f3..1706c8c87 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -8,14 +8,14 @@ Maximum number of memory map areas a process may have - u32:0-65535 + u32:65535-2147483647 Areas count - + - 4096 + 65535 diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 65dfac91a..f6c47d0c0 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -19,6 +19,7 @@ import psutil from vyos import ConfigError +from vyos.base import Warning from vyos.utils.cpu import get_core_count as total_core_count from vyos.vpp.control_host import get_eth_driver @@ -398,6 +399,8 @@ def verify_vpp_host_resources(config: dict): hugepages = mem_checks.get_hugepages_total() if max_map_count < 2 * hugepages: - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * HugePages_Total)' + Warning( + 'The max-map-count should be greater than or equal to (2 * HugePages_Total) ' + 'or VPP could work not properly. Please set up ' + f'"vpp settings host-resources max-map-count" to {2 * hugepages} or higher' ) -- cgit v1.2.3 From 5da6f4a639e2f0153f295ef1d6b6c68b077d52a6 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Fri, 1 Aug 2025 17:11:16 +0300 Subject: T7655: Change VPP memory verification Get Hugepages information from /sys/kernel/mm/hugepages instead of /proc/meminfo Add an error margin for memory calculations --- python/vyos/vpp/config_resource_checks/memory.py | 24 ++++----------- python/vyos/vpp/config_verify.py | 39 ++++++++++++------------ 2 files changed, 26 insertions(+), 37 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index 64fdf9abe..f8ccf7d47 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -28,7 +28,7 @@ from vyos.vpp.config_resource_checks.resource_defaults import default_resource_m def get_hugepages_info() -> dict: """ - Returns the information about HugePages + Returns the information about HugePages for default hugepage size retrieved from /proc/meminfo """ info = {} @@ -40,18 +40,18 @@ def get_hugepages_info() -> dict: return info -def get_total_hugepages_free_memory() -> int: +def get_total_hugepages_memory() -> int: """ - Returns the total amount of hugepage-backed free memory (in bytes) + Returns the total amount of hugepage memory (in bytes) """ info = get_hugepages_info() - hugepages_free = info.get('HugePages_Free') + hugepages_total = info.get('HugePages_Total') hugepage_size = info.get('Hugepagesize') * 1024 - return hugepage_size * hugepages_free + return hugepage_size * hugepages_total -def get_hugepages_total() -> int: +def get_total_hugepages_count() -> int: """ Returns the total count of hugepages """ @@ -69,18 +69,6 @@ def get_numa_count(): return int(m.group(1)) if m else 0 -def get_memory_from_kernel_settings(settings: dict) -> int: - hugepage_settings = settings.get('hugepage_size', {}) - - total_bytes = 0 - for size_str, info in hugepage_settings.items(): - count = int(info.get('hugepage_count', 0)) - page_bytes = human_memory_to_bytes(size_str) - total_bytes += count * page_bytes - - return total_bytes - - def buffer_size(settings: dict) -> int: numa_count = get_numa_count() buffers_per_numa = int( diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index f6c47d0c0..0e21a5a8a 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -226,27 +226,28 @@ def verify_vpp_memory(config: dict): f'The main heap size must be greater than or equal to page-size ({readable_heap_page})' ) - # Get available HugePage memory to compare with required memory for VPP - available_memory = mem_checks.get_total_hugepages_free_memory() - + available_memory = mem_checks.get_total_hugepages_memory() memory_required = mem_checks.total_memory_required(config['settings']) - # Check if there is a config currently active - # If yes, calculate how much memory it consumes - # and exclude it from required memory - if config.get('effective'): - memory_used = mem_checks.total_memory_required(config['effective']['settings']) - # If we want to reduce memory configs then there is nothing to check - if memory_used > memory_required: - return - memory_required -= memory_used - - if memory_required > available_memory: + if main_heap_size > available_memory: + available_memory_in_mb = bytes_to_human_memory(available_memory, 'M') + raise ConfigError( + f'"memory main-heap-size" must not be greater than hugepages memory. Reduce to {available_memory_in_mb} or less' + ) + + memory_required = round(memory_required / 1024**3, 1) + available_memory = round(available_memory / 1024**3, 1) + + # Allow 10% error margin + allowed_margin = memory_required * 0.1 + + # Compare HugePage memory with required memory for VPP + if memory_required > available_memory + allowed_margin: raise ConfigError( - 'Not enough free hugepage memory to start VPP: ' - f'available: {round(available_memory / 1024 ** 3, 1)} GB, ' - f'required: {round(memory_required / 1024 ** 3, 1)} GB. ' - 'Please add kernel memory options for HugePages and reboot' + f'Not enough free hugepage memory to start VPP: ' + f'available: {available_memory} GB, required: {memory_required} GB. ' + 'Please add kernel memory options for HugePages ' + '"set system option kernel memory hugepage-size ..." and reboot' ) @@ -396,7 +397,7 @@ def verify_vpp_host_resources(config: dict): max_map_count = int(config['settings']['host_resources']['max_map_count']) # Get HugePages total count - hugepages = mem_checks.get_hugepages_total() + hugepages = mem_checks.get_total_hugepages_count() if max_map_count < 2 * hugepages: Warning( -- cgit v1.2.3 From 6801f0ef7365556d1c8eb60aeed01789a7620cb7 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 7 Aug 2025 15:43:25 +0000 Subject: T7697: Remove year from copyright --- python/vyos/vpp/__init__.py | 2 +- python/vyos/vpp/acl/acl.py | 2 +- python/vyos/vpp/config_deps.py | 2 +- python/vyos/vpp/config_filter.py | 2 +- python/vyos/vpp/config_resource_checks/cpu.py | 2 +- python/vyos/vpp/config_resource_checks/memory.py | 2 +- python/vyos/vpp/config_resource_checks/resource_defaults.py | 2 +- python/vyos/vpp/config_verify.py | 2 +- python/vyos/vpp/configdb.py | 2 +- python/vyos/vpp/control_host.py | 2 +- python/vyos/vpp/control_vpp.py | 2 +- python/vyos/vpp/interface/__init__.py | 2 +- python/vyos/vpp/interface/bond.py | 2 +- python/vyos/vpp/interface/bridge.py | 2 +- python/vyos/vpp/interface/ethernet.py | 2 +- python/vyos/vpp/interface/geneve.py | 2 +- python/vyos/vpp/interface/gre.py | 2 +- python/vyos/vpp/interface/interface.py | 2 +- python/vyos/vpp/interface/ipip.py | 2 +- python/vyos/vpp/interface/loopback.py | 2 +- python/vyos/vpp/interface/vxlan.py | 2 +- python/vyos/vpp/interface/wireguard.py | 2 +- python/vyos/vpp/interface/xconnect.py | 2 +- python/vyos/vpp/nat/det44.py | 2 +- python/vyos/vpp/nat/nat44.py | 2 +- python/vyos/vpp/utils.py | 2 +- smoketest/scripts/cli/test_vpp.py | 2 +- src/completion/list_mem_page_size.py | 2 +- src/completion/list_vpp_interfaces.py | 2 +- src/conf_mode/vpp.py | 2 +- src/conf_mode/vpp_acl.py | 2 +- src/conf_mode/vpp_interfaces_bonding.py | 2 +- src/conf_mode/vpp_interfaces_bridge.py | 2 +- src/conf_mode/vpp_interfaces_ethernet.py | 2 +- src/conf_mode/vpp_interfaces_geneve.py | 2 +- src/conf_mode/vpp_interfaces_gre.py | 2 +- src/conf_mode/vpp_interfaces_ipip.py | 2 +- src/conf_mode/vpp_interfaces_loopback.py | 2 +- src/conf_mode/vpp_interfaces_vxlan.py | 2 +- src/conf_mode/vpp_interfaces_xconnect.py | 2 +- src/conf_mode/vpp_kernel-interfaces.py | 2 +- src/conf_mode/vpp_nat.py | 2 +- src/conf_mode/vpp_nat_cgnat.py | 2 +- src/conf_mode/vpp_sflow.py | 2 +- src/op_mode/show_vpp_interfaces.py | 2 +- src/op_mode/show_vpp_nat44.py | 2 +- src/op_mode/vpp_acl.py | 2 +- src/op_mode/vpp_nat_cgnat.py | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) (limited to 'python') diff --git a/python/vyos/vpp/__init__.py b/python/vyos/vpp/__init__.py index a320ac7d1..3352914f1 100644 --- a/python/vyos/vpp/__init__.py +++ b/python/vyos/vpp/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/acl/acl.py b/python/vyos/vpp/acl/acl.py index 9da241f1c..57d36ac91 100644 --- a/python/vyos/vpp/acl/acl.py +++ b/python/vyos/vpp/acl/acl.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py index d826dedbb..dffd865d1 100644 --- a/python/vyos/vpp/config_deps.py +++ b/python/vyos/vpp/config_deps.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023-2024 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_filter.py b/python/vyos/vpp/config_filter.py index 4ab9ffda2..dc864930b 100644 --- a/python/vyos/vpp/config_filter.py +++ b/python/vyos/vpp/config_filter.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2024 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py index ee0bace2d..b319608e4 100644 --- a/python/vyos/vpp/config_resource_checks/cpu.py +++ b/python/vyos/vpp/config_resource_checks/cpu.py @@ -1,6 +1,6 @@ # Used for validating estimated CPU/physical cores use # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index f8ccf7d47..bce2df2bb 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -1,6 +1,6 @@ # Used for memory consumption calculations # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py index 828683456..797b45701 100644 --- a/python/vyos/vpp/config_resource_checks/resource_defaults.py +++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py @@ -1,6 +1,6 @@ # Default values for resource consumption checks # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 0e21a5a8a..1beb3141b 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -1,6 +1,6 @@ # Used for verifying configuration vpp interfaces # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/configdb.py b/python/vyos/vpp/configdb.py index 1830937ad..13c1cd3a0 100644 --- a/python/vyos/vpp/configdb.py +++ b/python/vyos/vpp/configdb.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2024 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py index 1ac004fbb..6a6b9d72f 100644 --- a/python/vyos/vpp/control_host.py +++ b/python/vyos/vpp/control_host.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 5959b68b5..596ef4f12 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py index 2048e07ca..903b7cbfc 100644 --- a/python/vyos/vpp/interface/__init__.py +++ b/python/vyos/vpp/interface/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py index 6a96bce58..604ba2ae5 100644 --- a/python/vyos/vpp/interface/bond.py +++ b/python/vyos/vpp/interface/bond.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py index 5ebe46d45..25f0b4660 100644 --- a/python/vyos/vpp/interface/bridge.py +++ b/python/vyos/vpp/interface/bridge.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/ethernet.py b/python/vyos/vpp/interface/ethernet.py index 368d94c7f..523211db8 100644 --- a/python/vyos/vpp/interface/ethernet.py +++ b/python/vyos/vpp/interface/ethernet.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Ethernet interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py index 8260b1bfa..2d6347f12 100644 --- a/python/vyos/vpp/interface/geneve.py +++ b/python/vyos/vpp/interface/geneve.py @@ -1,6 +1,6 @@ # VyOS implementation of Geneve interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py index 5bef280c6..bc278d2a0 100644 --- a/python/vyos/vpp/interface/gre.py +++ b/python/vyos/vpp/interface/gre.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP GRE interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py index 33dc779c2..05c1d14d6 100644 --- a/python/vyos/vpp/interface/interface.py +++ b/python/vyos/vpp/interface/interface.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py index 4779a8292..e6ed44537 100644 --- a/python/vyos/vpp/interface/ipip.py +++ b/python/vyos/vpp/interface/ipip.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP IPIP interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py index d417f627d..357423780 100644 --- a/python/vyos/vpp/interface/loopback.py +++ b/python/vyos/vpp/interface/loopback.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Loopback interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py index eacc901dc..4be5b5ec9 100644 --- a/python/vyos/vpp/interface/vxlan.py +++ b/python/vyos/vpp/interface/vxlan.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP VXLAN interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/wireguard.py b/python/vyos/vpp/interface/wireguard.py index 649ed5b05..c2d36e1b0 100644 --- a/python/vyos/vpp/interface/wireguard.py +++ b/python/vyos/vpp/interface/wireguard.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP Wireguard interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py index badece6e8..299a61405 100644 --- a/python/vyos/vpp/interface/xconnect.py +++ b/python/vyos/vpp/interface/xconnect.py @@ -1,6 +1,6 @@ # VyOS implementation of VPP bridge interface # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/nat/det44.py b/python/vyos/vpp/nat/det44.py index 1046d7cf9..70f903086 100644 --- a/python/vyos/vpp/nat/det44.py +++ b/python/vyos/vpp/nat/det44.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index 6b02688aa..b7e2d7744 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index fe08a98b8..8b3a8c038 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 16302adea..07b4b1b19 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/completion/list_mem_page_size.py b/src/completion/list_mem_page_size.py index 1ed81a15f..4d2cb11c6 100644 --- a/src/completion/list_mem_page_size.py +++ b/src/completion/list_mem_page_size.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) VyOS Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/src/completion/list_vpp_interfaces.py b/src/completion/list_vpp_interfaces.py index ea6a77bed..e5751119c 100644 --- a/src/completion/list_vpp_interfaces.py +++ b/src/completion/list_vpp_interfaces.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS Inc. +# Copyright (C) VyOS Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 0ea8d261b..4fa056a19 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py index d6a9150d1..cbc915226 100644 --- a/src/conf_mode/vpp_acl.py +++ b/src/conf_mode/vpp_acl.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py index c9aa6276c..098cf8bed 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py index 4272db616..64722c10c 100644 --- a/src/conf_mode/vpp_interfaces_bridge.py +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py index f56cadc3e..81050444e 100644 --- a/src/conf_mode/vpp_interfaces_ethernet.py +++ b/src/conf_mode/vpp_interfaces_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py index 12fac83fb..efac4e65a 100644 --- a/src/conf_mode/vpp_interfaces_geneve.py +++ b/src/conf_mode/vpp_interfaces_geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index 5ec6feeb0..0b60d06b5 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py index 430b7b334..e4db1e93c 100644 --- a/src/conf_mode/vpp_interfaces_ipip.py +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py index 92ba110f1..8874ecf2d 100644 --- a/src/conf_mode/vpp_interfaces_loopback.py +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py index c4068818e..355139ffa 100644 --- a/src/conf_mode/vpp_interfaces_vxlan.py +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py index 220c1d44e..994332ba2 100644 --- a/src/conf_mode/vpp_interfaces_xconnect.py +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py index f9354c964..85706094e 100644 --- a/src/conf_mode/vpp_kernel-interfaces.py +++ b/src/conf_mode/vpp_kernel-interfaces.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py index 2c0298902..7c9fd2765 100644 --- a/src/conf_mode/vpp_nat.py +++ b/src/conf_mode/vpp_nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_nat_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py index 68695b78e..be2386ba2 100644 --- a/src/conf_mode/vpp_nat_cgnat.py +++ b/src/conf_mode/vpp_nat_cgnat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/conf_mode/vpp_sflow.py b/src/conf_mode/vpp_sflow.py index ebe91617b..7da4c24d6 100644 --- a/src/conf_mode/vpp_sflow.py +++ b/src/conf_mode/vpp_sflow.py @@ -1,4 +1,4 @@ -# Copyright (C) 2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as diff --git a/src/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py index b7c42eb58..c159ad0aa 100755 --- a/src/op_mode/show_vpp_interfaces.py +++ b/src/op_mode/show_vpp_interfaces.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS Inc. +# Copyright (C) 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 diff --git a/src/op_mode/show_vpp_nat44.py b/src/op_mode/show_vpp_nat44.py index d0569ac49..500280210 100644 --- a/src/op_mode/show_vpp_nat44.py +++ b/src/op_mode/show_vpp_nat44.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/op_mode/vpp_acl.py b/src/op_mode/vpp_acl.py index 7afe96433..ed9b35ef6 100644 --- a/src/op_mode/vpp_acl.py +++ b/src/op_mode/vpp_acl.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 diff --git a/src/op_mode/vpp_nat_cgnat.py b/src/op_mode/vpp_nat_cgnat.py index 5112c70be..9a7dd2086 100644 --- a/src/op_mode/vpp_nat_cgnat.py +++ b/src/op_mode/vpp_nat_cgnat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2025 VyOS Inc. +# Copyright (C) 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 -- cgit v1.2.3