diff options
| author | Daniil Baturin <daniil@vyos.io> | 2025-08-12 15:31:00 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-12 15:31:00 +0100 |
| commit | 703031e46b7a01f022b5b83766f4aa0bf4353963 (patch) | |
| tree | 7fba79247d7c1bc373d93c88809ede774b4cbf85 /src/conf_mode | |
| parent | 33b13061b55a8dbf302dcfaed1cf89b3e0f12e63 (diff) | |
| parent | 1a83bde3c14ff893960c3d2921279d82fdb21f76 (diff) | |
| download | vyos-1x-703031e46b7a01f022b5b83766f4aa0bf4353963.tar.gz vyos-1x-703031e46b7a01f022b5b83766f4aa0bf4353963.zip | |
Merge pull request #4650 from sever-sever/T7697
T7697: Merge vyos-vpp repo into vyos-1x
Diffstat (limited to 'src/conf_mode')
| -rwxr-xr-x | src/conf_mode/vpp.py | 695 | ||||
| -rw-r--r-- | src/conf_mode/vpp_acl.py | 400 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bonding.py | 234 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bridge.py | 193 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_ethernet.py | 134 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_geneve.py | 193 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_gre.py | 222 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_ipip.py | 211 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_loopback.py | 174 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_vxlan.py | 218 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_xconnect.py | 149 | ||||
| -rw-r--r-- | src/conf_mode/vpp_kernel-interfaces.py | 199 | ||||
| -rw-r--r-- | src/conf_mode/vpp_nat.py | 488 | ||||
| -rw-r--r-- | src/conf_mode/vpp_nat_cgnat.py | 218 | ||||
| -rw-r--r-- | src/conf_mode/vpp_sflow.py | 145 |
15 files changed, 3873 insertions, 0 deletions
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py new file mode 100755 index 000000000..6e02f9593 --- /dev/null +++ b/src/conf_mode/vpp.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python3 +# +# 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 +# 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 pyroute2.iproute import IPRoute + +try: + from vpp_papi import VPPIOError, VPPValueError +except ImportError: # pylint: disable=import-error + VPPIOError = VPPValueError = None + +from vyos import ConfigError +from vyos import airbag +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.ifconfig import Section +from vyos.template import render +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.process import call +from vyos.utils.system import sysctl_read, sysctl_apply + +from vyos.vpp import VPPControl +from vyos.vpp import control_host +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_dev_driver, + 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, + verify_vpp_host_resources, +) +from vyos.vpp.config_filter import iface_filter_eth +from vyos.vpp.utils import EthtoolGDrvinfo +from vyos.vpp.configdb import JSONStorage + +airbag.enable() + +service_name = 'vpp' +service_conf = Path(f'/run/vpp/{service_name}.conf') +systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' + +dependency_interface_type_map = { + 'vpp_interfaces_bonding': 'bonding', + 'vpp_interfaces_bridge': 'bridge', + 'vpp_interfaces_ethernet': 'ethernet', + 'vpp_interfaces_geneve': 'geneve', + 'vpp_interfaces_gre': 'gre', + 'vpp_interfaces_ipip': 'ipip', + 'vpp_interfaces_loopback': 'loopback', + 'vpp_interfaces_vxlan': 'vxlan', + 'vpp_interfaces_xconnect': 'xconnect', +} + +# dict of drivers that needs to be overrided +override_drivers: dict[str, str] = { + 'hv_netvsc': 'uio_hv_generic', + 'ena': 'vfio-pci', +} + +# drivers that does not use PCIe addresses +not_pci_drv: list[str] = ['hv_netvsc'] + +# drivers that support interrupt RX mode for DPDK and XDP +drivers_support_interrupt: dict[str, list] = { + 'atlantic': ['dpdk', 'xdp'], + 'bnx2x': ['dpdk'], + 'e1000': ['dpdk'], + 'ena': ['dpdk', 'xdp'], + 'i40e': ['dpdk', 'xdp'], + 'ice': ['dpdk', 'xdp'], + 'igb': ['xdp'], + 'igc': ['dpdk', 'xdp'], + 'ixgbe': ['dpdk', 'xdp'], + 'qede': ['dpdk', 'xdp'], + 'vmxnet3': ['xdp'], + 'virtio_net': ['xdp'], +} + + +def get_config(config=None): + # use persistent config to store interfaces data between executions + # this is required because some interfaces after they are connected + # to VPP is really hard or impossible to restore without knowing + # their original parameters (like IDs) + persist_config = JSONStorage('vpp_conf') + eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read( + 'eth_ifaces', {} + ) + + if config: + conf = config + else: + conf = Config() + + base = ['vpp'] + base_settings = ['vpp', 'settings'] + + # find interfaces removed from VPP + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + xconn_members = deps_xconnect_dict(conf) + + removed_ifaces = [] + tmp = node_changed(conf, base_settings + ['interface']) + if tmp: + for removed_iface in tmp: + to_append = { + 'iface_name': removed_iface, + 'driver': effective_config['settings']['interface'][removed_iface][ + 'driver' + ], + } + removed_ifaces.append(to_append) + # add an interface to a list of interfaces that need + # to be reinitialized after the commit + set_dependents('ethernet', conf, removed_iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # sFlow dependency + if conf.exists(['vpp', 'sflow']): + set_dependents('vpp_sflow', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + if not conf.exists(base): + return { + 'removed_ifaces': removed_ifaces, + 'xconn_members': xconn_members, + 'persist_config': eth_ifaces_persist, + } + + config = conf.get_config_dict( + base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + ) + + # Get default values which we need to conditionally update into the + # dictionary retrieved. + default_values = conf.get_config_defaults(**config.kwargs, recursive=True) + + # 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'] + + config = config_dict_merge(default_values, config) + + # Ignore default XML values if config doesn't exists + if not conf.exists(base_settings + ['ipsec']): + del config['settings']['ipsec'] + + # add running config + if effective_config: + config['effective'] = effective_config + + if 'settings' in config: + if 'interface' in config['settings']: + for iface, iface_config in config['settings']['interface'].items(): + # Driver must be configured to continue + if 'driver' not in iface_config: + raise ConfigError( + f'"driver" must be configured for {iface} interface!' + ) + + old_driver = leaf_node_changed( + conf, base_settings + ['interface', iface, 'driver'] + ) + + # Get current kernel module, required for extra verification and + # logic for VMBus interfaces + config['settings']['interface'][iface]['kernel_module'] = ( + EthtoolGDrvinfo(iface).driver + ) + + # filter unsupported config nodes + iface_filter_eth(conf, iface) + set_dependents('ethernet', conf, iface) + # Interfaces with changed driver should be removed/readded + if old_driver and old_driver[0] == 'dpdk': + removed_ifaces.append( + { + 'iface_name': iface, + 'driver': 'dpdk', + } + ) + + # Get PCI address or device ID + if iface_config['driver'] == 'dpdk': + if 'dpdk_options' not in iface_config: + iface_config['dpdk_options'] = {} + # Check in a persistent config first + id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get( + 'dev_id' + ) + if id_from_persisten_conf: + iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf + else: + try: + iface_to_search = iface + if old_driver and old_driver[0] == 'xdp': + iface_to_search = f'defunct_{iface}' + iface_config['dpdk_options']['dev_id'] = ( + control_host.get_dev_id(iface_to_search) + ) + except Exception: + # Return empty address if all attempts failed + # We will catch this in verify() + iface_config['dpdk_options']['dev_id'] = '' + # prepare XDP interface parameters + if iface_config['driver'] == 'xdp': + xdp_api_params = { + 'rxq_size': int(iface_config['xdp_options']['rx_queue_size']), + 'txq_size': int(iface_config['xdp_options']['tx_queue_size']), + } + if iface_config['xdp_options']['num_rx_queues'] == 'all': + xdp_api_params['rxq_num'] = 0 + else: + xdp_api_params['rxq_num'] = int( + iface_config['xdp_options']['num_rx_queues'] + ) + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['mode'] = 'zero-copy' + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['flags'] = 'no_syscall_lock' + iface_config['xdp_api_params'] = xdp_api_params + + if removed_ifaces: + config['removed_ifaces'] = removed_ifaces + config['xconn_members'] = xconn_members + + # Dependencies + for dependency, interface_type in dependency_interface_type_map.items(): + # if conf.exists(base + ['interfaces', interface_type]): + if effective_config.get('interfaces', {}).get(interface_type): + for iface, iface_config in ( + config.get('interfaces', {}).get(interface_type, {}).items() + ): + # filter unsupported config nodes + if interface_type == 'ethernet': + iface_filter_eth(conf, iface) + set_dependents(dependency, conf, iface) + + # Save important info about all interfaces that cannot be retrieved later + # Add new interfaces (only if they are first time seen in a config) + for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): + if iface not in effective_config.get('settings', {}).get('interface', {}): + eth_ifaces_persist[iface] = { + 'original_driver': config['settings']['interface'][iface][ + 'kernel_module' + ], + } + eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) + eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + + # 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(config): + # bail out early - looks like removal from running config + if not config or ('removed_ifaces' in config and 'settings' not in config): + return None + + if 'settings' not in config: + raise ConfigError('"settings interface" is required but not set!') + + 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 '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']) + + # ensure DPDK/XDP settings are properly configured + for iface, iface_config in config['settings']['interface'].items(): + # check if selected driver is supported, but only for new interfaces + if iface not in config.get('effective', {}).get('settings', {}).get( + 'interface', {} + ): + if not verify_dev_driver(iface, iface_config['driver']): + raise ConfigError( + f'Driver {iface_config["driver"]} is not compatible with interface {iface}!' + ) + if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config: + if iface_config['xdp_options']['num_rx_queues'] != 'all': + Warning(f'Not all RX queues will be connected to VPP for {iface}!') + + if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config: + raise ConfigError('DPDK options are not applicable for XDP driver!') + + 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': + # By default drivers operate in polling mode. Not all NIC drivers support + # RX mode interrupt and adaptive + driver = config.get('persist_config').get(iface).get('original_driver') + if ( + driver not in drivers_support_interrupt + or iface_config['driver'] not in drivers_support_interrupt[driver] + ): + raise ConfigError( + f'RX mode {rx_mode} is not supported for interface {iface}' + ) + + # check GRE tunnels as part of the bridge, only tunnel-type teb is allowed + # set vpp interfaces bridge br1 member interface gre1 + # set vpp interfaces gre gre1 tunnel-type teb + if 'interfaces' in config: + if 'bridge' in config['interfaces']: + for iface, iface_config in config['interfaces']['bridge'].items(): + if 'member' in iface_config: + for member in iface_config['member'].get('interface', []): + if member.startswith('gre'): + if ( + 'gre' in config['interfaces'] + and config['interfaces']['gre'] + .get(member, {}) + .get('tunnel_type') + != 'teb' + ): + raise ConfigError( + f'Only tunnel-type teb is allowed for GRE interfaces in bridge {iface}' + ) + + # Only one multipoint GRE tunnel is allowed from the same source address + # 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' + # set vpp interfaces gre gre1 mode 'point-to-multipoint' + # set vpp interfaces gre gre1 remote '0.0.0.0' + # set vpp interfaces gre gre1 source-address '192.0.2.1' + if 'gre' in config['interfaces']: + for iface, iface_config in config['interfaces']['gre'].items(): + if iface_config['mode'] == 'point-to-multipoint': + for other_iface, other_iface_config in config['interfaces'][ + 'gre' + ].items(): + if ( + other_iface_config['mode'] == 'point-to-multipoint' + and other_iface_config['source_address'] + == iface_config['source_address'] + and iface != other_iface + ): + raise ConfigError( + 'Only one multipoint GRE tunnel is allowed from the same source address' + ) + + # Check if deleted interfaces are not xconnect memebrs + for iface_config in config.get('removed_ifaces', []): + if iface_config['iface_name'] in config.get('xconn_members', {}): + raise ConfigError( + f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed' + ) + + +def generate(config): + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + render(service_conf, 'vpp/startup.conf.j2', config['settings']) + render(systemd_override, 'vpp/override.conf.j2', config) + + # apply sysctl values + # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf + # vm.nr_hugepages are now configured in section + # 'set system option kernel memory hugepage-size 2M hugepage-count <count>' + sysctl_config: dict[str, str] = { + 'vm.max_map_count': config['settings']['host_resources']['max_map_count'], + 'vm.hugetlb_shm_group': '0', + 'kernel.shmmax': config['settings']['host_resources']['shmmax'], + } + # we do not want to lower current values + for sysctl_key, sysctl_value in sysctl_config.items(): + # perform check only for quantitative params + if sysctl_key == 'vm.hugetlb_shm_group': + pass + current_value = sysctl_read(sysctl_key) + if int(current_value) > int(sysctl_value): + sysctl_config[sysctl_key] = current_value + + if not sysctl_apply(sysctl_config): + raise ConfigError('Cannot configure sysctl parameters for VPP') + + return None + + +def initialize_interface(iface, driver, iface_config) -> None: + # DPDK - rescan PCI to use a proper driver + if driver == 'dpdk' and iface_config['original_driver'] not in not_pci_drv: + control_host.pci_rescan(iface_config['dev_id']) + # rename to the proper name + iface_new_name: str = control_host.get_eth_name(iface_config['dev_id']) + control_host.rename_iface(iface_new_name, iface) + + # XDP - rename an interface, disable promisc and XDP + if driver == 'xdp': + control_host.set_promisc(f'defunct_{iface}', 'off') + control_host.rename_iface(f'defunct_{iface}', iface) + control_host.xdp_remove(iface) + + # Rename Mellanox NIC to a normal name + try: + if control_host.get_eth_driver(f'defunct_{iface}') == 'mlx5_core': + control_host.rename_iface(f'defunct_{iface}', iface) + except FileNotFoundError: + pass + + # Replace a driver with original for VMBus interfaces and rename it + if driver == 'dpdk' and iface_config['original_driver'] in override_drivers: + control_host.override_driver(iface_config['bus_id'], iface_config['dev_id']) + iface_new_name: str = control_host.get_eth_name(iface_config['dev_id']) + control_host.rename_iface(iface_new_name, iface) + + +def apply(config): + # Open persistent config + # It is required for operations with interfaces + persist_config = JSONStorage('vpp_conf') + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Cleanup persistent config + persist_config.delete() + # And stop the service + call(f'systemctl stop {service_name}.service') + else: + # Some interfaces required extra preparation before VPP can be started + if 'settings' in config and 'interface' in config.get('settings'): + for iface, iface_config in config['settings']['interface'].items(): + if iface_config['driver'] == 'dpdk': + # ena interfaces require noiommu mode + if iface_config['kernel_module'] == 'ena': + control_host.unsafe_noiommu_mode(True) + + if iface_config['kernel_module'] in override_drivers: + control_host.override_driver( + config['persist_config'][iface]['bus_id'], + config['persist_config'][iface]['dev_id'], + override_drivers[iface_config['kernel_module']], + ) + + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + + # Initialize interfaces removed from VPP + for iface in config.get('removed_ifaces', []): + initialize_interface( + iface['iface_name'], + iface['driver'], + config['persist_config'][iface['iface_name']], + ) + + # Remove what is not in the config anymore + if iface['iface_name'] not in config.get('settings', {}).get('interface', {}): + del config['persist_config'][iface['iface_name']] + + if 'settings' in config and 'interface' in config.get('settings'): + # connect to VPP + # must be performed multiple attempts because API is not available + # immediately after the service restart + try: + vpp_control = VPPControl(attempts=20, interval=500) + + # preconfigure LCP plugin + if 'ignore_kernel_routes' in config.get('settings', {}).get('lcp', {}): + vpp_control.cli_cmd('lcp param route-no-paths off') + else: + vpp_control.cli_cmd('lcp param route-no-paths on') + # add interfaces + iproute = IPRoute() + for iface, iface_config in config['settings']['interface'].items(): + # promisc option for DPDK interfaces + if iface_config['driver'] == 'dpdk': + if 'promisc' in iface_config['dpdk_options']: + if_index = vpp_control.get_sw_if_index(iface) + vpp_control.api.sw_interface_set_promisc( + sw_if_index=if_index, promisc_on=True + ) + # add XDP interfaces + if iface_config['driver'] == 'xdp': + control_host.rename_iface(iface, f'defunct_{iface}') + vpp_control.xdp_iface_create( + host_if=f'defunct_{iface}', + name=iface, + **iface_config['xdp_api_params'], + ) + # replicate MAC address of a real interface + real_mac = control_host.get_eth_mac(f'defunct_{iface}') + vpp_control.set_iface_mac(iface, real_mac) + if 'promisc' in iface_config['xdp_options']: + control_host.set_promisc(f'defunct_{iface}', 'on') + control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') + # Rename Mellanox interfaces to hide them and create LCP properly + if ( + iface in Section.interfaces() + and control_host.get_eth_driver(iface) == 'mlx5_core' + ): + control_host.rename_iface(iface, f'defunct_{iface}') + control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') + # Create lcp + if iface not in Section.interfaces(): + vpp_control.lcp_pair_add(iface, iface) + + # For unknown reasons, if multiple interfaces later try to be + # initialized by configuration scripts, some of them may stuck + # in an endless UP/DOWN loop + # We found two workarounds - pause initialization (requires + # main code modifications). + # And this one + dev_index = iproute.link_lookup(ifname=iface)[0] + iproute.link('set', index=dev_index, state='up') + + # Set rx-mode. Should be configured after interface state set to UP + rx_mode = iface_config.get('rx_mode') + if rx_mode: + # to hardware side + vpp_control.iface_rxmode(iface, rx_mode) + # to kernel side + lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get( + 'vpp_name_kernel' + ) + vpp_control.iface_rxmode(lcp_name, rx_mode) + + # 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_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) + + except (VPPIOError, VPPValueError) as e: + # if cannot connect to VPP or an error occurred then + # we need to stop vpp service and initialize interfaces + call(f'systemctl stop {service_name}.service') + for iface, iface_config in config['settings']['interface'].items(): + initialize_interface( + iface, iface_config['driver'], config['persist_config'][iface] + ) + + raise ConfigError( + f'An error occurred: {e}. ' + 'VPP service will be restarted with the previous configuration' + ) + + # Save persistent config + if 'persist_config' in config and config['persist_config']: + persist_config.write('eth_ifaces', config['persist_config']) + + # reinitialize interfaces, but not during the first boot + if boot_configuration_complete(): + call_dependents() + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py new file mode 100644 index 000000000..cbc915226 --- /dev/null +++ b/src/conf_mode/vpp_acl.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# +# 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 +# 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, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # 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 or 'remove_vpp' 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): + if 'remove_vpp' in config: + return None + + 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 new file mode 100644 index 000000000..098cf8bed --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos import ConfigError + +from vyos.vpp.interface import BondInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_xconnect_interface, + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_list, cli_ifaces_lcp_kernel_list + + +def _get_bond_mode(mode_name: str) -> int: + """Convert VyOS CLI name bonding mode to VPP compatible""" + mode_mapping = { + 'round-robin': 1, + 'active-backup': 2, + 'xor-hash': 3, + 'broadcast': 4, + '802.3ad': 5, + } + + return mode_mapping.get(mode_name, 5) + + +def _get_bond_lb(lb_name: str) -> int: + """Convert VyOS CLI name bonding load balance to VPP compatible""" + lb_mapping = { + 'layer2': 0, + 'layer2+3': 2, + 'layer3+4': 1, + } + + return lb_mapping.get(lb_name, 5) + + +def get_config(config=None) -> dict: + """Get Bonding interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bonding interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bonding'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # convert values to VPP compatible + if 'mode' in config: + config['mode'] = _get_bond_mode(config['mode']) + if 'hash_policy' in config: + config['hash_policy'] = _get_bond_lb(config['hash_policy']) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if conf.exists(base + [ifname, 'kernel-interface']): + if effective_config.get('kernel_interface') or __name__ != '__main__': + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # 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 + + +def verify(config): + if 'remove_vpp' in config: + return None + + verify_vpp_remove_kernel_interface(config) + verify_vpp_remove_xconnect_interface(config) + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for bonding') + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # remove old members + if 'effective' in config: + members = config['effective'].get('member', {}).get('interface', []) + + kernel_interface = config['effective'].get('kernel_interface', '') + i = BondInterface(ifname, kernel_interface=kernel_interface) + for member in members: + i.detach_member(interface=member) + + if 'kernel_interface' in config['effective']: + i.kernel_delete() + # Delete bonding interface + i.delete() + + if 'remove' in config: + return None + + # Create a new one + mode = config.get('mode') + lb = config.get('hash_policy') + members = config.get('member', {}).get('interface', []) + mac = config.get('mac', '') + kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' + + i = BondInterface(ifname, mode, lb, mac, kernel_interface, state) + i.add() + # Add members to bond + if members: + for member in members: + i.add_member(interface=member) + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py new file mode 100644 index 000000000..64722c10c --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos import ConfigError +from vyos.vpp.interface import BridgeInterface +from vyos.vpp.utils import iftunnel_transform + + +def get_config(config=None) -> dict: + """Get Bridge interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bridge'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # determine which members have been removed + interfaces_removed = node_changed(conf, base + [ifname, 'member', 'interface']) + if interfaces_removed: + config['members_removed'] = interfaces_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Check if interface exists in vpp before adding to bridge-domain + + allowed_prefixes = ('bond', 'gre', 'geneve', 'lo', 'vxlan') + + if 'member' in config: + 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', {}) + 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 + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # vxlan10 in the vpp is vxlan_tunnel10 + interface_transform_filter = ('geneve', 'vxlan') + # update members + if 'members_removed' in config: + i = BridgeInterface(ifname) + for member in config.get('members_removed'): + if member.startswith(interface_transform_filter): + member = iftunnel_transform(member) + if member.startswith('lo'): + # interface name in VPP is loopX + member = member.replace('lo', 'loop') + elif member.startswith('bond'): + # interface name in VPP is BondEthernetX + member = member.replace('bond', 'BondEthernet') + i.detach_member(member=member) + + # Delete bridge domain + if 'effective' in config: + ifname = config.get('ifname') + i = BridgeInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add bridge domain + members = config.get('member', {}).get('interface', '') + i = BridgeInterface(ifname) + i.add() + # Add members to bridge + if members: + br = BridgeInterface(ifname) + port_type = 0 + for member, member_config in members.items(): + 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') + if 'bvi' in member_config: + port_type = 1 + elif member.startswith('bond'): + # interface name in VPP is BondEthernetX + member = member.replace('bond', 'BondEthernet') + + br.add_member(member=member, port_type=port_type) + # set default port type 0 (not BVI) + port_type = 0 + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py new file mode 100644 index 000000000..81050444e --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ethernet.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdep import set_dependents +from vyos import ConfigError +from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface + + +def get_config(config=None) -> dict: + """Get Ethernet interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Ethernet interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ethernet'] + base_interfaces_xconnect = ['vpp', 'interfaces', 'xconnect'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not conf.exists(base + [ifname]): + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + config.update({'remove': effective_config}) + else: + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get global 'vpp interfaces xconnect' + config['vpp_interfaces_xconnect'] = conf.get_config_dict( + base_interfaces_xconnect, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # Dependency + for xcon_name, xcon_config in config.get('vpp_interfaces_xconnect').items(): + for member_name in xcon_config.get('member', {}).get('interface', []): + if member_name == ifname: + set_dependents('vpp_interfaces_xconnect', conf, xcon_name) + + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + verify_vpp_remove_xconnect_interface(config) + if 'remove' in config: + return None + + +def generate(config): + pass + + +def apply(config): + # Delete interface + if 'remove' in config: + pass + # remove_config = config.get('remove') + # ifname = config.get('ifname') + # src_addr = remove_config.get('source_address') + # dst_addr = remove_config.get('remote') + # vni = int(remove_config.get('vni')) + # v = VXLANInterface(ifname, src_addr, dst_addr, vni) + # v.delete() + else: + pass + # ifname = config.get('ifname') + # kernel_interface = config.get('kernel_interface', '') + # v = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + # v.add() + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py new file mode 100644 index 000000000..efac4e65a --- /dev/null +++ b/src/conf_mode/vpp_interfaces_geneve.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GeneveInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None) -> dict: + """Get Geneve interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Geneve interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'geneve'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces geneve geneveX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces geneve geneveX kernel-interface vpp-tunX' + # => 'vpp interfaces geneve geneveX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = GeneveInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + i = GeneveInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py new file mode 100644 index 000000000..0b60d06b5 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GREInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get GRE interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: GRE interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'gre'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # 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']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # 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 + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces gre greX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote', '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('_', '-')}" + ) + + # 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 + if config.get('remote') != '0.0.0.0': + raise ConfigError('For point-to-multipoint mode, remote must be 0.0.0.0') + + # Change 'vpp interfaces gre greX kernel-interface vpp-tunX' + # => 'vpp interfaces gre greX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = GREInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + mode = config.get('mode') + tunnel_type = config.get('tunnel_type') + 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 + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py new file mode 100644 index 000000000..e4db1e93c --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents + +from vyos.vpp.interface import IPIPInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get IPIP interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: IPIP interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ipip'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # 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']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if config.get('effective', {}).get('kernel_interface'): + iface = config.get('kernel_interface') + if iface: + if iface in config.get('vpp_kernel_interfaces'): + set_dependents('vpp_kernel_interface', conf, iface) + + # 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 + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces ipip ipipX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # 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' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = IPIPInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + 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: + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py new file mode 100644 index 000000000..8874ecf2d --- /dev/null +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import LoopbackInterface +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None) -> dict: + """Get Loopback interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Loopback interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'loopback'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # 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 + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + verify_vpp_remove_kernel_interface(config) + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + i = LoopbackInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add interface + kernel_interface = config.get('kernel_interface', '') + 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 + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py new file mode 100644 index 000000000..355139ffa --- /dev/null +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import VXLANInterface +from vyos.vpp.config_deps import deps_bridge_dict +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get VXLAN interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: VXLAN interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'vxlan'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # 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']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + config['bridge_members'] = deps_bridge_dict(conf) + if ifname in config['bridge_members']: + for bridge_iface in config['bridge_members'][ifname]: + set_dependents('vpp_interfaces_bridge', conf, bridge_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # 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 + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces vxlan vxlanX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # 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' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = VXLANInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + 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 + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py new file mode 100644 index 000000000..994332ba2 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos import ConfigError +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.interface import XconnectInterface +from vyos.vpp.utils import cli_ifaces_list + + +def get_config(config=None) -> dict: + """Get Xconnect interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'xconnect'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + config['xconn_members'] = deps_xconnect_dict(conf) + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Xconnect requires 2 members + if len(config.get('member', {}).get('interface')) != 2: + raise ConfigError('Cross connect requires 2 members') + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for xconnect') + + # Each interface can belong only to one xconnect + for xconn_member, xconn_ifaces in config['xconn_members'].items(): + if len(xconn_ifaces) > 1: + raise ConfigError( + f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + + # Delete xconnect + if 'effective' in config: + remove_config = config.get('effective') + members = remove_config.get('member', {}).get('interface') + i = XconnectInterface(ifname, members=members) + i.del_l2_xconnect() + + if 'remove' in config: + return None + + # Add xconnect + members = config.get('member', {}).get('interface') + state = 'up' if 'disable' not in config else 'down' + i = XconnectInterface(ifname, members=members, state=state) + i.add_l2_xconnect() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py new file mode 100644 index 000000000..85706094e --- /dev/null +++ b/src/conf_mode/vpp_kernel-interfaces.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdict import node_changed +from vyos import ConfigError +from vyos.ifconfig import Interface +from vyos.utils.network import interface_exists +from vyos.utils.process import call +from vyos.vpp import VPPControl + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + if __name__ == '__main__': + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + # if a file was started as dependency, we are starting from empty config + else: + effective_config = {} + + if effective_config: + config.update({'effective': effective_config}) + + address_removed = leaf_node_changed(conf, base + [ifname, 'address']) + if address_removed: + config['address_removed'] = address_removed + + description_removed = leaf_node_changed(conf, base + [ifname, 'description']) + if description_removed: + config['description_removed'] = {} + + vlans_removed = node_changed(conf, base + [ifname, 'vif']) + if vlans_removed: + config['vlans_removed'] = vlans_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Interface must exists before it is configured + if not interface_exists(config['ifname']): + raise ConfigError( + f'Interface {config["ifname"]} must be created before using in configuration' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + i = Interface(ifname) + # update/remove addresses + if 'address_removed' in config: + for address in config['address_removed']: + i.del_addr(address) + # remove description + if 'description_removed' in config: + i.set_alias('') + + # remove VLANs + if 'vlans_removed' in config: + for vlan in config['vlans_removed']: + call(f'ip link del dev {ifname}.{vlan}') + + # Delete + if 'remove' in config: + pass + else: + # Add address + if 'address' in config: + for address in config['address']: + i.add_addr(address) + # Set MTU + if 'mtu' in config: + i.set_mtu(config.get('mtu')) + # Set description + if 'description' in config: + i.set_alias(config.get('description')) + # Admin state down + if 'disable' in config: + i.set_admin_state('down') + else: + i.set_admin_state('up') + + for vlan, vlan_config in config.get('vif', {}).items(): + if vlan not in config.get('effective', {}).get('vif', {}).keys(): + call( + f'ip link add link {ifname} name {ifname}.{vlan} type vlan id {vlan}' + ) + call(f'ip link set dev {ifname}.{vlan} up') + v = Interface(f'{ifname}.{vlan}') + + # VLAN address + addresses_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('address', []) + ) + addresses_candidate = vlan_config.get('address', []) + + for ipaddr in addresses_effective: + if ipaddr not in addresses_candidate: + v.del_addr(ipaddr) + for ipaddr in addresses_candidate: + if ipaddr not in addresses_effective: + v.add_addr(ipaddr) + + # VLAN description + description_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('description', '') + ) + description_candidate = vlan_config.get('description', '') + + if description_candidate: + v.set_alias(description_candidate) + elif description_effective and not description_candidate: + v.set_alias('') + + v.set_admin_state('up') + + # Set rx-mode + rx_mode = config.get('rx_mode') + if rx_mode: + vpp_control = VPPControl() + lcp_name = vpp_control.lcp_pair_find(kernel_name=ifname).get('vpp_name_kernel') + vpp_control.iface_rxmode(lcp_name, rx_mode) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py new file mode 100644 index 000000000..7c9fd2765 --- /dev/null +++ b/src/conf_mode/vpp_nat.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +# +# 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 +# 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, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # 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 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}) + + 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 or 'remove_vpp' 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): + if 'remove_vpp' in config: + return None + + 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 '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__': + 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_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py new file mode 100644 index 000000000..be2386ba2 --- /dev/null +++ b/src/conf_mode/vpp_nat_cgnat.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# +# 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 +# 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, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # 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 or 'remove_vpp' 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): + if 'remove_vpp' in config: + return None + + 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/conf_mode/vpp_sflow.py b/src/conf_mode/vpp_sflow.py new file mode 100644 index 000000000..7da4c24d6 --- /dev/null +++ b/src/conf_mode/vpp_sflow.py @@ -0,0 +1,145 @@ +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from vyos import ConfigError +from vyos.config import Config +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp import VPPControl + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'sflow'] + + # 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, + ) + + # Get system sflow configuration to check for server + system_sflow = conf.get_config_dict( + ['system', 'sflow'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if system_sflow: + config['system_sflow'] = system_sflow + + if not config: + config['remove'] = True + return config + + # Add list of VPP interfaces to the config + config.update({'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 + + # Check if interface section exists + if 'interface' not in config: + return None + + # Verify that all interfaces specified exist in VPP + for interface in config['interface']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for sFlow monitoring' + ) + + # Verify sample rate is a positive integer + if 'sample_rate' in config: + try: + sample_rate = int(config['sample_rate']) + if sample_rate <= 0: + raise ConfigError('sFlow sample rate must be a positive integer') + except ValueError: + raise ConfigError('sFlow sample rate must be a valid integer') + + # Verify that system sflow has enable-vpp defined + if 'system_sflow' not in config or 'vpp' not in config.get('system_sflow', {}): + raise ConfigError( + 'sFlow enable-vpp must be defined under system sflow configuration' + ) + + +def generate(config): + # No templates to render for sFlow + pass + + +def apply(config): + # Initialize VPP control API + vpp = VPPControl(attempts=20, interval=500) + + if 'remove' in config: + # Disable sFlow on all interfaces + for interface in config.get('effective', {}).get('interface', []): + vpp.cli_cmd(f'sflow enable-disable {interface} disable') + return None + + # Configure sample rate if specified + if 'sample_rate' in config: + vpp.cli_cmd(f'sflow sampling-rate {config["sample_rate"]}') + + # Configure interfaces + if 'interface' in config: + # Enable sFlow on specified interfaces + for interface in config['interface']: + vpp.cli_cmd(f'sflow enable-disable {interface}') + + # Disable sFlow on interfaces that were removed from config + effective_interfaces = config.get('effective', {}).get('interface', []) + if effective_interfaces: + for interface in effective_interfaces: + if interface not in config['interface']: + vpp.cli_cmd(f'sflow enable-disable {interface} disable') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |
