diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/completion/list_vpp_interfaces.py | 39 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 553 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bonding.py | 215 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bridge.py | 158 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_ethernet.py | 135 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_geneve.py | 182 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_gre.py | 182 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_ipip.py | 181 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_loopback.py | 153 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_vxlan.py | 188 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_xconnect.py | 142 | ||||
| -rw-r--r-- | src/conf_mode/vpp_kernel-interfaces.py | 192 | ||||
| -rwxr-xr-x | src/op_mode/show_vpp_interfaces.py | 257 |
13 files changed, 2577 insertions, 0 deletions
diff --git a/src/completion/list_vpp_interfaces.py b/src/completion/list_vpp_interfaces.py new file mode 100644 index 000000000..ea6a77bed --- /dev/null +++ b/src/completion/list_vpp_interfaces.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl +from vyos.vpp.utils import vpp_ifaces_list + + +def get_vpp_ifaces_names(): + config = ConfigTreeQuery() + if not config.exists('vpp settings interface'): + return [] + + vpp = VPPControl() + vpp_ifaces = vpp_ifaces_list(vpp.api) + ifaces_names = [iface['interface_name'] for iface in vpp_ifaces] + + return sorted(ifaces_names) + + +if __name__ == "__main__": + ifaces = [] + ifaces = get_vpp_ifaces_names() + print(" ".join(ifaces)) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py new file mode 100755 index 000000000..32dd0d01a --- /dev/null +++ b/src/conf_mode/vpp.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from pathlib import Path + +from psutil import virtual_memory +from pyroute2 import IPRoute + +from vyos import ConfigError +from vyos import airbag +from vyos.base import Warning +from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents +from vyos.configdict import node_changed, leaf_node_changed +from vyos.utils.cpu import get_core_count +from vyos.ifconfig import Section +from vyos.template import render +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.process import call +from vyos.utils.system import sysctl_read, sysctl_apply + +from vyos.vpp import VPPControl +from vyos.vpp import control_host +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import verify_dev_driver +from vyos.vpp.config_filter import iface_filter_eth +from vyos.vpp.utils import EthtoolGDrvinfo +from vyos.vpp.configdb import JSONStorage + +airbag.enable() + +service_name = 'vpp' +service_conf = Path(f'/run/vpp/{service_name}.conf') +systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' + +dependency_interface_type_map = { + 'vpp_interfaces_bonding': 'bonding', + 'vpp_interfaces_bridge': 'bridge', + 'vpp_interfaces_ethernet': 'ethernet', + 'vpp_interfaces_geneve': 'geneve', + 'vpp_interfaces_gre': 'gre', + 'vpp_interfaces_ipip': 'ipip', + 'vpp_interfaces_loopback': 'loopback', + 'vpp_interfaces_vxlan': 'vxlan', + 'vpp_interfaces_xconnect': 'xconnect', +} + +# dict of drivers that needs to be overrided +override_drivers: dict[str, str] = { + 'hv_netvsc': 'uio_hv_generic', + 'ena': 'vfio-pci', +} + +# drivers that does not use PCIe addresses +not_pci_drv: list[str] = ['hv_netvsc'] + + +def get_config(config=None): + # use persistent config to store interfaces data between executions + # this is required because some interfaces after they are connected + # to VPP is really hard or impossible to restore without knowing + # their original parameters (like IDs) + persist_config = JSONStorage('vpp_conf') + eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read( + 'eth_ifaces', {} + ) + + if config: + conf = config + else: + conf = Config() + + base = ['vpp'] + base_settings = ['vpp', 'settings'] + + # find interfaces removed from VPP + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + xconn_members = deps_xconnect_dict(conf) + + removed_ifaces = [] + tmp = node_changed(conf, base_settings + ['interface']) + if tmp: + for removed_iface in tmp: + to_append = { + 'iface_name': removed_iface, + 'driver': effective_config['settings']['interface'][removed_iface][ + 'driver' + ], + } + removed_ifaces.append(to_append) + # add an interface to a list of interfaces that need + # to be reinitialized after the commit + set_dependents('ethernet', conf, removed_iface) + + if not conf.exists(base): + return { + 'removed_ifaces': removed_ifaces, + 'xconn_members': xconn_members, + 'persist_config': eth_ifaces_persist, + } + + config = conf.get_config_dict( + base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + # add running config + if effective_config: + config['effective'] = effective_config + + if 'settings' in config: + if 'interface' in config['settings']: + for iface, iface_config in config['settings']['interface'].items(): + # Driver must be configured to continue + if 'driver' not in iface_config: + raise ConfigError( + f'"driver" must be configured for {iface} interface!' + ) + + old_driver = leaf_node_changed( + conf, base_settings + ['interface', iface, 'driver'] + ) + + # Get current kernel module, required for extra verification and + # logic for VMBus interfaces + config['settings']['interface'][iface]['kernel_module'] = ( + EthtoolGDrvinfo(iface).driver + ) + + # filter unsupported config nodes + iface_filter_eth(conf, iface) + set_dependents('ethernet', conf, iface) + # Interfaces with changed driver should be removed/readded + if old_driver and old_driver[0] == 'dpdk': + removed_ifaces.append( + { + 'iface_name': iface, + 'driver': 'dpdk', + } + ) + + # Get PCI address or device ID + if iface_config['driver'] == 'dpdk': + if 'dpdk_options' not in iface_config: + iface_config['dpdk_options'] = {} + # Check in a persistent config first + id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get( + 'dev_id' + ) + if id_from_persisten_conf: + iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf + else: + try: + iface_to_search = iface + if old_driver and old_driver[0] == 'xdp': + iface_to_search = f'defunct_{iface}' + iface_config['dpdk_options']['dev_id'] = ( + control_host.get_dev_id(iface_to_search) + ) + except Exception: + # Return empty address if all attempts failed + # We will catch this in verify() + iface_config['dpdk_options']['dev_id'] = '' + # prepare XDP interface parameters + if iface_config['driver'] == 'xdp': + xdp_api_params = { + 'rxq_size': int(iface_config['xdp_options']['rx_queue_size']), + 'txq_size': int(iface_config['xdp_options']['tx_queue_size']), + } + if iface_config['xdp_options']['num_rx_queues'] == 'all': + xdp_api_params['rxq_num'] = 0 + else: + xdp_api_params['rxq_num'] = int( + iface_config['xdp_options']['num_rx_queues'] + ) + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['mode'] = 'zero-copy' + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['flags'] = 'no_syscall_lock' + iface_config['xdp_api_params'] = xdp_api_params + + if removed_ifaces: + config['removed_ifaces'] = removed_ifaces + config['xconn_members'] = xconn_members + + # Dependencies + for dependency, interface_type in dependency_interface_type_map.items(): + # if conf.exists(base + ['interfaces', interface_type]): + if effective_config.get('interfaces', {}).get(interface_type): + for iface, iface_config in ( + config.get('interfaces', {}).get(interface_type, {}).items() + ): + # filter unsupported config nodes + if interface_type == 'ethernet': + iface_filter_eth(conf, iface) + set_dependents(dependency, conf, iface) + + # Save important info about all interfaces that cannot be retrieved later + # Add new interfaces (only if they are first time seen in a config) + for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): + if iface not in effective_config.get('settings', {}).get('interface', {}): + eth_ifaces_persist[iface] = { + 'original_driver': config['settings']['interface'][iface][ + 'kernel_module' + ], + } + eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) + eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + + # Return to config dictionary + config['persist_config'] = eth_ifaces_persist + + return config + + +def convert_to_int(val): + rates = { + 'K': 1024, + 'M': 1024**2, + 'G': 1024**3, + } + try: + return int(val) + except ValueError: + return int(val[:-1]) * rates[val[-1]] + + +def verify_memory(settings): + memory_available: int = virtual_memory().available + cpus: int = get_core_count() + + nr_hugepages = int(settings['host_resources']['nr_hugepages']) + hugepages_memory = nr_hugepages * 2 * 1024**2 + memory_required = hugepages_memory + + buffers_per_numa = int(settings.get('buffers', {}).get('buffers_per_numa', 16384)) + data_size = int(settings.get('buffers', {}).get('data_size', 2048)) + buffers_memory = buffers_per_numa * data_size * cpus + + memory_required += buffers_memory + + netlink_buffer_size = int( + settings.get('lcp', {}).get('netlink', {}).get('rx_buffer_size', 212992) + ) + memory_required += netlink_buffer_size + + memory_main_heap = convert_to_int( + settings.get('memory', {}).get('main_heap_size', '1G') + ) + memory_required += memory_main_heap + + statseg_size = convert_to_int(settings.get('statseg', {}).get('size', '96M')) + memory_required += statseg_size + + if memory_available < memory_required: + raise ConfigError( + 'Not enough free memory to start VPP:\n' + f'available: {round(memory_available / 1024 ** 3, 1)}GB\n' + f'required: {round(memory_required / 1024 ** 3, 1)}GB' + ) + + +def verify(config): + # bail out early - looks like removal from running config + if not config or ('removed_ifaces' in config and 'settings' not in config): + return None + + if 'settings' not in config: + raise ConfigError('"settings interface" is required but not set!') + + # CPU main-core must be not included to corelist-workers + if config.get('settings').get('cpu', {}).get('main_core') and config.get( + 'settings' + ).get('cpu', {}).get('corelist_workers'): + corelist_workers = config['settings']['cpu']['corelist_workers'] + main_core = int(config['settings']['cpu']['main_core']) + + all_core_numbers = [] + for worker_range in corelist_workers: + core_numbers = worker_range.split('-') + all_core_numbers.extend( + range(int(core_numbers[0]), int(core_numbers[-1]) + 1) + ) + + if main_core in all_core_numbers: + raise ConfigError( + f'"cpu main-core {main_core}" must not be included in the corelist-workers!' + ) + + if 'interface' not in config['settings']: + raise ConfigError('"settings interface" is required but not set!') + + # check if Ethernet interfaces exist + ethernet_ifaces = Section.interfaces('ethernet') + for iface in config['settings']['interface'].keys(): + if iface not in ethernet_ifaces: + raise ConfigError(f'Interface {iface} does not exist or is not Ethernet!') + + # ensure DPDK/XDP settings are properly configured + for iface, iface_config in config['settings']['interface'].items(): + # check if selected driver is supported, but only for new interfaces + if iface not in config.get('effective', {}).get('settings', {}).get( + 'interface', {} + ): + if not verify_dev_driver(iface, iface_config['driver']): + raise ConfigError( + f'Driver {iface_config["driver"]} is not compatible with interface {iface}!' + ) + if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config: + if iface_config['xdp_options']['num_rx_queues'] != 'all': + Warning(f'Not all RX queues will be connected to VPP for {iface}!') + + if 'cpu' in config['settings']: + if ( + 'corelist_workers' in config['settings']['cpu'] + and 'main_core' not in config['settings']['cpu'] + ): + raise ConfigError('"cpu main-core" is required but not set!') + + verify_memory(config['settings']) + + # Check if deleted interfaces are not xconnect memebrs + for iface_config in config.get('removed_ifaces', []): + if iface_config['iface_name'] in config.get('xconn_members', {}): + raise ConfigError( + f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed' + ) + + +def generate(config): + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + render(service_conf, 'vpp/startup.conf.j2', config['settings']) + render(systemd_override, 'vpp/override.conf.j2', config) + + # apply sysctl values + # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf + sysctl_config: dict[str, str] = { + 'vm.nr_hugepages': config['settings']['host_resources']['nr_hugepages'], + 'vm.max_map_count': config['settings']['host_resources']['max_map_count'], + 'vm.hugetlb_shm_group': '0', + 'kernel.shmmax': config['settings']['host_resources']['shmmax'], + } + # we do not want to lower current values + for sysctl_key, sysctl_value in sysctl_config.items(): + # perform check only for quantitative params + if sysctl_key == 'vm.hugetlb_shm_group': + pass + current_value = sysctl_read(sysctl_key) + if int(current_value) > int(sysctl_value): + sysctl_config[sysctl_key] = current_value + + if not sysctl_apply(sysctl_config): + raise ConfigError('Cannot configure sysctl parameters for VPP') + + return None + + +def apply(config): + # Open persistent config + # It is required for operations with interfaces + persist_config = JSONStorage('vpp_conf') + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Cleanup persistent config + persist_config.delete() + # And stop the service + call(f'systemctl stop {service_name}.service') + else: + # Some interfaces required extra preparation before VPP can be started + if 'settings' in config and 'interface' in config.get('settings'): + for iface, iface_config in config['settings']['interface'].items(): + if iface_config['driver'] == 'dpdk': + # ena interfaces require noiommu mode + if iface_config['kernel_module'] == 'ena': + control_host.unsafe_noiommu_mode(True) + + if iface_config['kernel_module'] in override_drivers: + control_host.override_driver( + config['persist_config'][iface]['bus_id'], + config['persist_config'][iface]['dev_id'], + override_drivers[iface_config['kernel_module']], + ) + + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + + # Initialize interfaces removed from VPP + for iface in config.get('removed_ifaces', []): + # DPDK - rescan PCI to use a proper driver + if ( + iface['driver'] == 'dpdk' + and config['persist_config'][iface['iface_name']]['original_driver'] + not in not_pci_drv + ): + control_host.pci_rescan( + config['persist_config'][iface['iface_name']]['dev_id'] + ) + # rename to the proper name + iface_new_name: str = control_host.get_eth_name( + config['persist_config'][iface['iface_name']]['dev_id'] + ) + control_host.rename_iface(iface_new_name, iface['iface_name']) + # XDP - rename an interface , disable promisc and XDP + if iface['driver'] == 'xdp': + control_host.set_promisc(f'defunct_{iface["iface_name"]}', 'off') + control_host.rename_iface( + f'defunct_{iface["iface_name"]}', iface['iface_name'] + ) + control_host.xdp_remove(iface['iface_name']) + # Rename Mellanox NIC to a normal name + try: + if ( + control_host.get_eth_driver(f'defunct_{iface["iface_name"]}') + == 'mlx5_core' + ): + control_host.rename_iface( + f'defunct_{iface["iface_name"]}', iface['iface_name'] + ) + except FileNotFoundError: + pass + # Replace a driver with original for VMBus interfaces and rename it + if ( + iface['driver'] == 'dpdk' + and config['persist_config'][iface['iface_name']]['original_driver'] + in override_drivers + ): + control_host.override_driver( + config['persist_config'][iface['iface_name']]['bus_id'], + config['persist_config'][iface['iface_name']]['dev_id'], + ) + iface_new_name: str = control_host.get_eth_name( + config['persist_config'][iface['iface_name']]['dev_id'] + ) + control_host.rename_iface(iface_new_name, iface['iface_name']) + + # Remove what is not in the config anymore + if iface['iface_name'] not in config.get('settings', {}).get('interface', {}): + del config['persist_config'][iface['iface_name']] + + if 'settings' in config and 'interface' in config.get('settings'): + # connect to VPP + # must be performed multiple attempts because API is not available + # immediately after the service restart + vpp_control = VPPControl(attempts=20, interval=500) + # preconfigure LCP plugin + if 'route_no_paths' in config.get('settings', {}).get('lcp', {}): + vpp_control.cli_cmd('lcp param route-no-paths on') + else: + vpp_control.cli_cmd('lcp param route-no-paths off') + # add interfaces + iproute = IPRoute() + for iface, iface_config in config['settings']['interface'].items(): + # promisc option for DPDK interfaces + if iface_config['driver'] == 'dpdk': + if 'promisc' in iface_config['dpdk_options']: + if_index = vpp_control.get_sw_if_index(iface) + vpp_control.api.sw_interface_set_promisc( + sw_if_index=if_index, promisc_on=True + ) + # add XDP interfaces + if iface_config['driver'] == 'xdp': + control_host.rename_iface(iface, f'defunct_{iface}') + vpp_control.xdp_iface_create( + host_if=f'defunct_{iface}', + name=iface, + **iface_config['xdp_api_params'], + ) + # replicate MAC address of a real interface + real_mac = control_host.get_eth_mac(f'defunct_{iface}') + vpp_control.set_iface_mac(iface, real_mac) + if 'promisc' in iface_config['xdp_options']: + control_host.set_promisc(f'defunct_{iface}', 'on') + control_host.set_status(f'defunct_{iface}', 'up') + # Rename Mellanox interfaces to hide them and create LCP properly + if ( + iface in Section.interfaces() + and control_host.get_eth_driver(iface) == 'mlx5_core' + ): + control_host.rename_iface(iface, f'defunct_{iface}') + control_host.set_status(f'defunct_{iface}', 'up') + # Create lcp + if iface not in Section.interfaces(): + vpp_control.lcp_pair_add(iface, iface) + + # Set rx-mode + rx_mode = iface_config.get('rx_mode') + if rx_mode: + # to hardware side + vpp_control.iface_rxmode(iface, rx_mode) + # to kernel side + lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get( + 'vpp_name_kernel' + ) + vpp_control.iface_rxmode(lcp_name, rx_mode) + + # For unknown reasons, if multiple interfaces later try to be + # initialized by configuration scripts, some of them may stuck + # in an endless UP/DOWN loop + # We found two workarounds - pause initialization (requires + # main code modifications). + # And this one + dev_index = iproute.link_lookup(ifname=iface)[0] + iproute.link('set', index=dev_index, state='up') + + # Syncronize routes via LCP + vpp_control.lcp_resync() + + # Save persistent config + if 'persist_config' in config and config['persist_config']: + persist_config.write('eth_ifaces', config['persist_config']) + + # reinitialize interfaces, but not during the first boot + if boot_configuration_complete(): + call_dependents() + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py new file mode 100644 index 000000000..9b057f992 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos import ConfigError + +from vyos.vpp.interface import BondInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_xconnect_interface, + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_list, cli_ifaces_lcp_kernel_list + + +def _get_bond_mode(mode_name: str) -> int: + """Convert VyOS CLI name bonding mode to VPP compatible""" + mode_mapping = { + 'round-robin': 1, + 'active-backup': 2, + 'xor-hash': 3, + 'broadcast': 4, + '802.3ad': 5, + } + + return mode_mapping.get(mode_name, 5) + + +def _get_bond_lb(lb_name: str) -> int: + """Convert VyOS CLI name bonding load balance to VPP compatible""" + lb_mapping = { + 'layer2': 0, + 'layer2+3': 2, + 'layer3+4': 1, + } + + return lb_mapping.get(lb_name, 5) + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Bonding interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Bonding interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bonding'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + if __name__ == '__main__': + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + # if a file was started as dependency, we are starting from empty config + else: + effective_config = {} + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # convert values to VPP compatible + if 'mode' in config: + config['mode'] = _get_bond_mode(config['mode']) + if 'hash_policy' in config: + config['hash_policy'] = _get_bond_lb(config['hash_policy']) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if conf.exists(base + [ifname, 'kernel-interface']): + if effective_config.get('kernel_interface') or __name__ != '__main__': + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + + return config + + +def verify(config): + verify_vpp_remove_kernel_interface(config) + verify_vpp_remove_xconnect_interface(config) + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for bonding') + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # remove old members + if 'effective' in config: + members = config['effective'].get('member', {}).get('interface', []) + + kernel_interface = config['effective'].get('kernel_interface', '') + i = BondInterface(ifname, kernel_interface=kernel_interface) + for member in members: + i.detach_member(interface=member) + + if 'kernel_interface' in config['effective']: + i.kernel_delete() + # Delete bonding interface + i.delete() + + if 'remove' in config: + return None + + # Create a new one + mode = config.get('mode') + lb = config.get('hash_policy') + members = config.get('member', {}).get('interface', []) + mac = config.get('mac', '') + kernel_interface = config.get('kernel_interface', '') + + i = BondInterface(ifname, mode, lb, mac, kernel_interface) + i.add() + # Add members to bond + if members: + for member in members: + i.add_member(interface=member) + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py new file mode 100644 index 000000000..c76d88480 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos import ConfigError +from vyos.vpp.interface import BridgeInterface +from vyos.vpp.utils import iftunnel_transform + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Bridge interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bridge'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # determine which members have been removed + interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) + if interfaces_removed: + config['members_removed'] = interfaces_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config: + return None + + # Check if interface exists in vpp before adding to bridge-domain + + allowed_prefixes = ('gre', 'geneve', 'vxlan') + + if 'member' in config: + for member in config.get('member', {}).get('interface', []): + # Check if the interface is explicitly listed or starts with allowed prefixes + if not ( + member in config.get('vpp_interfaces', []) + or member.startswith(allowed_prefixes) + ): + raise ConfigError( + f"Interface '{member}' not found in 'vpp settings interface' or does not start with allowed prefixes {allowed_prefixes}" + ) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # vxlan10 in the vpp is vxlan_tunnel10 + interface_transform_filter = ('geneve', 'vxlan') + # update members + if 'members_removed' in config: + i = BridgeInterface(ifname) + for member in config.get('members_removed'): + if member.startswith(interface_transform_filter): + member = iftunnel_transform(member) + i.detach_member(member=member) + + # Delete bridge domain + if 'effective' in config: + ifname = config.get('ifname') + i = BridgeInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add bridge domain + members = config.get('member', {}).get('interface', '') + i = BridgeInterface(ifname) + i.add() + # Add members to bridge + if members: + br = BridgeInterface(ifname) + for member in members: + if member.startswith(interface_transform_filter): + member = iftunnel_transform(member) + br.add_member(member=member) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py new file mode 100644 index 000000000..d82acb891 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ethernet.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdep import set_dependents +from vyos import ConfigError +from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Ethernet interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Ethernet interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ethernet'] + base_interfaces_xconnect = ['vpp', 'interfaces', 'xconnect'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not conf.exists(base + [ifname]): + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + config.update({'remove': effective_config}) + else: + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get global 'vpp interfaces xconnect' + config['vpp_interfaces_xconnect'] = conf.get_config_dict( + base_interfaces_xconnect, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # Dependency + for xcon_name, xcon_config in config.get('vpp_interfaces_xconnect').items(): + for member_name in xcon_config.get('member', {}).get('interface', []): + if member_name == ifname: + set_dependents('vpp_interfaces_xconnect', conf, xcon_name) + + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + verify_vpp_remove_xconnect_interface(config) + if 'remove' in config: + return None + + +def generate(config): + pass + + +def apply(config): + # Delete interface + if 'remove' in config: + pass + # remove_config = config.get('remove') + # ifname = config.get('ifname') + # src_addr = remove_config.get('source_address') + # dst_addr = remove_config.get('remote') + # vni = int(remove_config.get('vni')) + # v = VXLANInterface(ifname, src_addr, dst_addr, vni) + # v.delete() + else: + pass + # ifname = config.get('ifname') + # kernel_interface = config.get('kernel_interface', '') + # v = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + # v.add() + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py new file mode 100644 index 000000000..a40cc9c84 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_geneve.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GeneveInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Geneve interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Geneve interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'geneve'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces geneve geneveX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces geneve geneveX kernel-interface vpp-tunX' + # => 'vpp interfaces geneve geneveX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = GeneveInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + i = GeneveInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py new file mode 100644 index 000000000..b27234d18 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GREInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get GRE interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: GRE interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'gre'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces gre greX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces gre greX kernel-interface vpp-tunX' + # => 'vpp interfaces gre greX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = GREInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + i = GREInterface(ifname, src_addr, dst_addr, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py new file mode 100644 index 000000000..e0f1258d0 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents + +from vyos.vpp.interface import IPIPInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get IPIP interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: IPIP interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ipip'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if config.get('effective', {}).get('kernel_interface'): + iface = config.get('kernel_interface') + if iface: + if iface in config.get('vpp_kernel_interfaces'): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + + return config + + +def verify(config): + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces ipip ipipX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces ipip ipipX kernel-interface vpp-tunX' + # => 'vpp interfaces ipip ipipX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = IPIPInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + i = IPIPInterface(ifname, src_addr, dst_addr, kernel_interface) + i.add() + + if 'kernel_interface' in config: + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py new file mode 100644 index 000000000..67fd049f0 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import LoopbackInterface +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Loopback interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Loopback interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'loopback'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + verify_vpp_remove_kernel_interface(config) + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + i = LoopbackInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add interface + kernel_interface = config.get('kernel_interface', '') + i = LoopbackInterface(ifname, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py new file mode 100644 index 000000000..5c957c7fa --- /dev/null +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import VXLANInterface +from vyos.vpp.config_deps import deps_bridge_dict +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get VXLAN interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: VXLAN interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'vxlan'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + config['bridge_members'] = deps_bridge_dict(conf) + if ifname in config['bridge_members']: + for bridge_iface in config['bridge_members'][ifname]: + set_dependents('vpp_interfaces_bridge', conf, bridge_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces vxlan vxlanX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces vxlan greX kernel-interface vpp-tunX' + # => 'vpp interfaces gre vxlanX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = VXLANInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + i = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py new file mode 100644 index 000000000..dd1348b7c --- /dev/null +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos import ConfigError +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.interface import XconnectInterface +from vyos.vpp.utils import cli_ifaces_list + + +def get_config(config=None, recursive_defaults=True) -> dict: + """Get Xconnect interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'xconnect'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=recursive_defaults, + ) + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + config['xconn_members'] = deps_xconnect_dict(conf) + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config: + return None + + # Xconnect requires 2 members + if len(config.get('member', {}).get('interface')) != 2: + raise ConfigError('Cross connect requires 2 members') + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for xconnect') + + # Each interface can belong only to one xconnect + for xconn_member, xconn_ifaces in config['xconn_members'].items(): + if len(xconn_ifaces) > 1: + raise ConfigError( + f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}' + ) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + + # Delete xconnect + if 'effective' in config: + remove_config = config.get('effective') + members = remove_config.get('member', {}).get('interface') + i = XconnectInterface(ifname, members=members) + i.del_l2_xconnect() + + if 'remove' in config: + return None + + # Add xconnect + members = config.get('member', {}).get('interface') + i = XconnectInterface(ifname, members=members) + i.add_l2_xconnect() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py new file mode 100644 index 000000000..4e4ecb442 --- /dev/null +++ b/src/conf_mode/vpp_kernel-interfaces.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdict import node_changed +from vyos import ConfigError +from vyos.ifconfig import Interface +from vyos.utils.network import interface_exists +from vyos.utils.process import call +from vyos.vpp import VPPControl + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + ) + + # Get effective config as we need full dicitonary per interface delete + if __name__ == '__main__': + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + # if a file was started as dependency, we are starting from empty config + else: + effective_config = {} + + if effective_config: + config.update({'effective': effective_config}) + + address_removed = leaf_node_changed(conf, base + [ifname, 'address']) + if address_removed: + config['address_removed'] = address_removed + + description_removed = leaf_node_changed(conf, base + [ifname, 'description']) + if description_removed: + config['description_removed'] = {} + + vlans_removed = node_changed(conf, base + [ifname, 'vif']) + if vlans_removed: + config['vlans_removed'] = vlans_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config: + return None + + # Interface must exists before it is configured + if not interface_exists(config['ifname']): + raise ConfigError( + f'Interface {config["ifname"]} must be created before using in configuration' + ) + + +def generate(config): + pass + + +def apply(config): + ifname = config.get('ifname') + i = Interface(ifname) + # update/remove addresses + if 'address_removed' in config: + for address in config['address_removed']: + i.del_addr(address) + # remove description + if 'description_removed' in config: + i.set_alias('') + + # remove VLANs + if 'vlans_removed' in config: + for vlan in config['vlans_removed']: + call(f'ip link del dev {ifname}.{vlan}') + + # Delete + if 'remove' in config: + pass + else: + # Add address + if 'address' in config: + for address in config['address']: + i.add_addr(address) + # Set MTU + if 'mtu' in config: + i.set_mtu(config.get('mtu')) + # Set description + if 'description' in config: + i.set_alias(config.get('description')) + # Admin state down + if 'disable' in config: + i.set_admin_state('down') + else: + i.set_admin_state('up') + + for vlan, vlan_config in config.get('vif', {}).items(): + if vlan not in config.get('effective', {}).get('vif', {}).keys(): + call( + f'ip link add link {ifname} name {ifname}.{vlan} type vlan id {vlan}' + ) + call(f'ip link set dev {ifname}.{vlan} up') + v = Interface(f'{ifname}.{vlan}') + + # VLAN address + addresses_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('address', []) + ) + addresses_candidate = vlan_config.get('address', []) + + for ipaddr in addresses_effective: + if ipaddr not in addresses_candidate: + v.del_addr(ipaddr) + for ipaddr in addresses_candidate: + if ipaddr not in addresses_effective: + v.add_addr(ipaddr) + + # VLAN description + description_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('description', '') + ) + description_candidate = vlan_config.get('description', '') + + if description_candidate: + v.set_alias(description_candidate) + elif description_effective and not description_candidate: + v.set_alias('') + + v.set_admin_state('up') + + # Set rx-mode + rx_mode = config.get('rx_mode') + if rx_mode: + vpp_control = VPPControl() + lcp_name = vpp_control.lcp_pair_find(kernel_name=ifname).get('vpp_name_kernel') + vpp_control.iface_rxmode(lcp_name, rx_mode) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py new file mode 100755 index 000000000..b7c42eb58 --- /dev/null +++ b/src/op_mode/show_vpp_interfaces.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import argparse +import json +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.utils.process import rc_cmd + +from vyos.vpp import VPPControl +from vyos.vpp.utils import ( + vpp_ifaces_list, + vpp_ip_addresses_by_index, + vpp_ifaces_stats, +) + + +def get_iproute_address_list(interface: str) -> list: + """Get data from the Linux command 'ip --json address list dev {interface}' and return a list info + for the given interface. + + Args: + interface (str): Interface name. + + Returns: + list: A dictionary containing the JSON data from the 'ip --json address list' command for the specified interface. + """ + rc, out = rc_cmd(f'ip --json address list dev {interface}') + if rc: + return [] + return json.loads(out) + + +def get_iproute_link_list(interface): + """Get data from the Linux command 'ip --json link show dev {interface}' and return a list info + for the given interface. + + Args: + interface (str): Interface name. + + Returns: + list: A dictionary containing the JSON data from the 'ip --json link show' command for the specified interface. + """ + rc, out = rc_cmd(f'ip --json link list dev {interface}') + if rc: + return [] + return json.loads(out) + + +def merge_dicts(*dicts) -> dict: + """Merge dictionaries into a new dictionary. + + Args: + *dicts: Any number of dictionaries. + + Returns: + dict: A new dictionary containing all the key-value pairs from the given dictionaries. + """ + merged = {} + for dictionary in dicts: + merged.update(dictionary) + return merged + + +def show_interfaces(interfaces_list: list) -> str: + """Get JSON info from linux and represent it in a table format + Use tabulate to generate table + + Interface IP Address Mtu S/L Description + --------- ---------- --- --- ----------- + dum0 203.0.113.1/32 1500 u/u + 100.64.1.1/24 + eth0 192.168.122.14/24 1500 u/u WAN + + :return: + """ + table = [] + for interface in interfaces_list: + # Get the data for the interface + ip_address_data = get_iproute_address_list(interface) + link_data = get_iproute_link_list(interface) + + # Skip this interface if data is not available + if not link_data: + continue + interface_data = merge_dicts(ip_address_data[0], link_data[0]) + + # Get the interface name + interface_name = interface_data['ifname'] + + # Get the IP addresses and their corresponding prefixes + ip_info = [ + (address['local'], address.get('prefixlen', '')) + for address in interface_data['addr_info'] + ] + + # Format the IP addresses with prefixes and line breaks + ip_addresses = '\n'.join(f'{ip}/{prefix}' for ip, prefix in ip_info) + + # Get the MAC address + mac = interface_data.get('address', 'n/a') + + # Get the MTU + mtu = interface_data.get('mtu') + + # Get the state of the interface + state = interface_data['operstate'].lower() + + # Get the description of the interface + description = interface_data.get('ifalias', '') + + # Create the list of values for the table + values = [interface_name, ip_addresses, mac, mtu, state, description] + + # Append the list of values to the table + table.append(values) + + # Print the table with IP addresses listed on separate lines + headers = ['Interface', 'IP Address', 'MAC', 'MTU', 'State', 'Description'] + return tabulate(table, headers=headers, tablefmt='simple') + + +def show_interfaces_dataplane(interfaces_list: list, filter_type: str = 'all') -> str: + table = [] + interface_dp_filter = ('tun', 'tap') + lcp_pair_list = vpp.lcp_pairs_list() + vpp_name_kernel_to_kernel_name = { + entry['vpp_name_kernel']: entry['kernel_name'] for entry in lcp_pair_list + } + for interface in interfaces_list: + interface_name = interface.get('interface_name') + if filter_type == 'no_tun_tap' and interface_name.startswith( + interface_dp_filter + ): + continue + if filter_type == 'only_tun_tap' and not interface_name.startswith( + interface_dp_filter + ): + continue + kernel_name = vpp_name_kernel_to_kernel_name.get(interface_name, '') + + dp_ip_addresses = vpp_ip_addresses_by_index( + vpp.api, interface.get('sw_if_index') + ) + ip_addresses = '\n'.join(dp_ip_addresses) + + mac = str(interface.get('l2_address', 'n/a')) + mtu = interface.get('mtu', [])[0] + # state + flags = interface.get('flags') + state = 'up' if flags == 3 else 'down' + + iftype = interface.get('interface_dev_type').split()[0] + + values = [kernel_name, interface_name, iftype, ip_addresses, mac, mtu, state] + table.append(values) + headers = [ + 'Kernel', + 'Dataplane', + 'Type', + 'IP Address', + 'MAC', + 'MTU', + 'State', + ] + table = sorted(table) + return tabulate(table, headers=headers, tablefmt='simple') + + +def show_interfaces_hardware(intf_name) -> str: + if not intf_name: + intf_name = '' + + statistics = vpp_ifaces_stats(intf_name) + for intf, stats in sorted(statistics.items()): + print(f'\n---------------------------------\nInterface {intf}:\n') + table = [] + for k, v in stats.items(): + if isinstance(v, dict): + for i, j in v.items(): + table.append([f"{k} {i}", j]) + else: + table.append([k, v]) + print(tabulate(table, tablefmt="presto")) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Show VPP interfaces') + parser.add_argument( + '--summary', + action='store_true', + help='Show summary of VPP interfaces (ethernet and kernel tun)', + ) + parser.add_argument( + '--dataplane', action='store_true', help='Show VPP ethernet interfaces' + ) + parser.add_argument( + '--kernel', action='store_true', help='Show VPP kernel interfaces' + ) + parser.add_argument( + '--iproute', action='store_true', help='Show interfaces (iproute2)' + ) + parser.add_argument( + '--hardware', + action='store_true', + help='Show more detailed statistics for VPP interfaces', + ) + parser.add_argument('--intf-name', action='store', help='Kernel interface name') + + args = parser.parse_args() + + config = ConfigTreeQuery() + + if not config.exists('vpp settings interface'): + print('VPP interfaces not configured') + exit(0) + + vpp = VPPControl() + dp_ifaces_list = vpp_ifaces_list(vpp.api) + + if args.summary: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='all')) + + if args.dataplane: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='no_tun_tap')) + exit(0) + + if args.kernel: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='only_tun_tap')) + + if args.iproute: + vpp_interfaces = [] + vpp_ethernet = config.list_nodes('vpp settings interface') + vpp_interfaces.extend(vpp_ethernet) + if config.exists('vpp kernel-interfaces'): + vpp_kernel_interfaces = config.list_nodes('vpp kernel-interfaces') + vpp_interfaces.extend(vpp_kernel_interfaces) + print(show_interfaces(interfaces_list=vpp_interfaces)) + + if args.hardware: + show_interfaces_hardware(intf_name=args.intf_name) |
