diff options
| author | Daniil Baturin <daniil@vyos.io> | 2025-11-18 16:13:15 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-18 16:13:15 +0000 |
| commit | e4fbf90a16aa4494b5ad8a131f454edb2508e8f6 (patch) | |
| tree | 3448b453e3a279f0f7be65ec2ca3712817b22998 /src/conf_mode | |
| parent | 51ec3e9e6fa27506c0864c3c67379f66aceb012c (diff) | |
| parent | 0cc39141dba576609480f6707d16964de4a8a6bc (diff) | |
| download | vyos-1x-e4fbf90a16aa4494b5ad8a131f454edb2508e8f6.tar.gz vyos-1x-e4fbf90a16aa4494b5ad8a131f454edb2508e8f6.zip | |
Merge pull request #4845 from vyos/T7556
T7556: VPP add IPFIX collector configuration
Diffstat (limited to 'src/conf_mode')
| -rwxr-xr-x | src/conf_mode/vpp.py | 36 | ||||
| -rw-r--r-- | src/conf_mode/vpp_ipfix.py | 175 |
2 files changed, 211 insertions, 0 deletions
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index bd7ff173f..abb68847e 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -193,6 +193,33 @@ def _get_max_xdp_rx_queues(config: dict): return 1 +def _check_removed_interfaces(config: dict, feature_name: str, interfaces_config: dict): + """ + Check if removed interfaces are used in any feature configuration + + Args: + config: The main configuration dictionary + feature_name: Human-readable feature name for error messages + interfaces_config: The interfaces dictionary from the feature config + Example: + _check_removed_interfaces(config, 'IPFIX monitoring', config.get('ipfix', {}).get('interface', {})) + """ + if ( + 'removed_ifaces' not in config + or not config['removed_ifaces'] + or not interfaces_config + ): + return + + for removed_iface in config['removed_ifaces']: + iface_name = removed_iface.get('iface_name') + if iface_name and iface_name in interfaces_config: + raise ConfigError( + f'Cannot remove interface {iface_name} - it is currently configured for {feature_name}. ' + f'Remove it from {feature_name} configuration first.' + ) + + def get_config(config=None): # use persistent config to store interfaces data between executions # this is required because some interfaces after they are connected @@ -419,6 +446,10 @@ def get_config(config=None): if conf.exists(['vpp', 'acl']): set_dependents('vpp_acl', conf) + # IPFIX dependency + if conf.exists(['vpp', 'ipfix']): + set_dependents('vpp_ipfix', conf) + # PPPoE dependency if pppoe_map_ifaces: config['pppoe_ifaces'] = pppoe_map_ifaces @@ -442,6 +473,11 @@ def verify(config): 'Disable PPPoE control-plane integration with VPP before proceeding.' ) + # Check remove VPP interface that used in IPFIX + _check_removed_interfaces( + config, 'IPFIX monitoring', config.get('ipfix', {}).get('interface', {}) + ) + # bail out early - looks like removal from running config if not config or ('removed_ifaces' in config and 'settings' not in config): return None diff --git a/src/conf_mode/vpp_ipfix.py b/src/conf_mode/vpp_ipfix.py new file mode 100644 index 000000000..a6659257c --- /dev/null +++ b/src/conf_mode/vpp_ipfix.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# +# 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.ipfix import IPFIX +from vyos.vpp.utils import cli_ifaces_list + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'ipfix'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base): + config['remove'] = True + return config + + # Add list of VPP interfaces to the config + config.update({'vpp_ifaces': cli_ifaces_list(conf)}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + # Verify that at least one interface is configured + if 'interface' not in config or not config['interface']: + raise ConfigError( + 'At least one interface must be configured for IPFIX monitoring' + ) + + # 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 IPFIX monitoring' + ) + + # Verify that at least one collector is configured + if 'collector' not in config: + raise ConfigError('At least one IPFIX collector must be configured') + + # Enforce that only one collector is configured (VPP limitation) + if len(config['collector']) > 1: + raise ConfigError('Only one IPFIX collector can be configured') + + # Verify that source_address is specified + for c, c_conf in config.get('collector', {}).items(): + if 'source_address' not in c_conf: + raise ConfigError(f'Source address must be specified for collector {c}') + + # Verify active timeout is not greater than inactive timeout + if 'active_timeout' in config and 'inactive_timeout' in config: + active_timeout = int(config['active_timeout']) + inactive_timeout = int(config['inactive_timeout']) + + if active_timeout > inactive_timeout: + raise ConfigError( + f'Active timeout ({active_timeout}) cannot be greater than inactive timeout ({inactive_timeout})' + ) + + +def generate(config): + # No templates to render for IPFIX + pass + + +def apply(config): + i = IPFIX() + + # Remove collectors + for c, c_conf in config.get('effective', {}).get('collector', {}).items(): + i.ipfix_exporter_delete() + + # Remove interfaces + for iface, iface_conf in config.get('effective', {}).get('interface', {}).items(): + direction = iface_conf.get('direction') + which = iface_conf.get('flow_variant') + i.flowprobe_interface_delete(iface, direction=direction, which=which) + + if 'remove' in config: + return None + + active_timeout = config.get('active_timeout') + inactive_timeout = config.get('inactive_timeout') + flowprobe_record = config.get('flowprobe_record') + + # Flowprobe params + i.flowprobe_set_params( + active_timer=int(active_timeout), + passive_timer=int(inactive_timeout), + record_flags=list(flowprobe_record), + ) + + # Collectors + for c, c_conf in config.get('collector', {}).items(): + collector_address = c + collector_port = c_conf.get('port') + src_address = c_conf.get('source_address') + template_interval = c_conf.get('template_interval') + path_mtu = c_conf.get('path_mtu') + udp_checksum = 'udp_checksum' in c_conf + + i.collector_address = collector_address + i.src_address = src_address + i.collector_port = int(collector_port) + i.template_interval = int(template_interval) + i.path_mtu = int(path_mtu) + i.udp_checksum = udp_checksum + # VRF support is not currently implemented; exporter is always configured in the default VRF (0). + # Consider adding VRF support in the future if needed. + i.vrf_id = 0 + + i.set_ipfix_exporter() + + # Interfaces + if 'interface' in config: + for iface, iface_config in config.get('interface', {}).items(): + direction = iface_config.get('direction') + which = iface_config.get('flow_variant') + + i.flowprobe_interface_add(iface, direction=direction, which=which) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |
