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 | |
| 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
| -rw-r--r-- | data/config-mode-dependencies/vyos-vpp.json | 1 | ||||
| -rw-r--r-- | data/op-mode-standardized.json | 1 | ||||
| -rw-r--r-- | data/templates/vpp/startup.conf.j2 | 2 | ||||
| -rw-r--r-- | interface-definitions/vpp.xml.in | 176 | ||||
| -rw-r--r-- | op-mode-definitions/vpp_ipfix.xml.in | 36 | ||||
| -rw-r--r-- | python/vyos/vpp/ipfix/__init__.py | 3 | ||||
| -rw-r--r-- | python/vyos/vpp/ipfix/ipfix.py | 181 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpp.py | 89 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 36 | ||||
| -rw-r--r-- | src/conf_mode/vpp_ipfix.py | 175 | ||||
| -rwxr-xr-x | src/op_mode/vpp.py | 165 |
11 files changed, 865 insertions, 0 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 0f8d6521c..54136f779 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -11,6 +11,7 @@ "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], "vpp_acl": ["vpp_acl"], + "vpp_ipfix": ["vpp_ipfix"], "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"], diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 5d3f4a249..d1f23bfb0 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -32,5 +32,6 @@ "system.py", "uptime.py", "version.py", +"vpp.py", "vrf.py" ] diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index edfb5b36d..6cec6ae81 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -91,6 +91,8 @@ plugins { plugin linux_cp_plugin.so { enable } plugin linux_nl_plugin.so { enable } plugin pppoe_plugin.so { enable } + # Flow + plugin flowprobe_plugin.so { enable } plugin sflow_plugin.so { enable } # NAT uncomment if needed # plugin cnat_plugin.so { enable } diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index e10a3409d..749fb3fbf 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -346,6 +346,182 @@ </tagNode> </children> </node> + <node name="ipfix" owner="${vyos_conf_scripts_dir}/vpp_ipfix.py"> + <properties> + <help>IP Flow Information Export (IPFIX) configuration</help> + <priority>322</priority> + </properties> + <children> + <tagNode name="collector"> + <properties> + <help>Collector IP address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 server to export IPFIX</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 server to export IPFIX</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>4739</defaultValue> + </leafNode> + <leafNode name="path-mtu"> + <properties> + <help>Path MTU</help> + <valueHelp> + <format>u32:68-1450</format> + <description>Bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-1450"/> + </constraint> + </properties> + <defaultValue>512</defaultValue> + </leafNode> + #include <include/source-address-ipv4-ipv6.xml.i> + <leafNode name="template-interval"> + <properties> + <help>Interval in seconds</help> + <valueHelp> + <format>u32:1-300</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-300"/> + </constraint> + </properties> + <defaultValue>20</defaultValue> + </leafNode> + <leafNode name="udp-checksum"> + <properties> + <help>Allow UDP checksum</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="direction"> + <properties> + <help>Flow direction</help> + <completionHelp> + <list>rx tx both</list> + </completionHelp> + <valueHelp> + <format>rx</format> + <description>Rx direction</description> + </valueHelp> + <valueHelp> + <format>tx</format> + <description>Tx direction</description> + </valueHelp> + <valueHelp> + <format>both</format> + <description>Rx and Tx direction</description> + </valueHelp> + <constraint> + <regex>(rx|tx|both)</regex> + </constraint> + </properties> + <defaultValue>both</defaultValue> + </leafNode> + <leafNode name="flow-variant"> + <properties> + <help>Flow variant</help> + <completionHelp> + <list>l2 ipv4 ipv6</list> + </completionHelp> + <valueHelp> + <format>l2</format> + <description>L2</description> + </valueHelp> + <valueHelp> + <format>_ipv4</format> + <description>IPv4</description> + </valueHelp> + <valueHelp> + <format>_ipv6</format> + <description>IPv6</description> + </valueHelp> + <constraint> + <regex>(l2|ipv4|ipv6)</regex> + </constraint> + </properties> + <defaultValue>ipv4</defaultValue> + </leafNode> + </children> + </tagNode> + <leafNode name="active-timeout"> + <properties> + <help>Flow activity timeout</help> + <valueHelp> + <format>u32:0-2147483647</format> + <description>Active flow export timeout (seconds)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647"/> + </constraint> + </properties> + <defaultValue>15</defaultValue> + </leafNode> + <leafNode name="inactive-timeout"> + <properties> + <help>Flow inactivity timeout</help> + <valueHelp> + <format>u32:0-2147483647</format> + <description>Inactive flow export timeout (seconds)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647"/> + </constraint> + </properties> + <defaultValue>120</defaultValue> + </leafNode> + <leafNode name="flowprobe-record"> + <properties> + <help>Flow record layers</help> + <completionHelp> + <list>l2 l3 l4</list> + </completionHelp> + <valueHelp> + <format>l2</format> + <description>Include level 2 information</description> + </valueHelp> + <valueHelp> + <format>l3</format> + <description>Include level 3 information</description> + </valueHelp> + <valueHelp> + <format>l4</format> + <description>Include level 4 information</description> + </valueHelp> + <constraint> + <regex>(l2|l3|l4)</regex> + </constraint> + <multi/> + </properties> + <defaultValue>l3</defaultValue> + </leafNode> + </children> + </node> <node name="settings"> <properties> <help>VPP settings</help> diff --git a/op-mode-definitions/vpp_ipfix.xml.in b/op-mode-definitions/vpp_ipfix.xml.in new file mode 100644 index 000000000..90e860e2d --- /dev/null +++ b/op-mode-definitions/vpp_ipfix.xml.in @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpp"> + <children> + <node name="ipfix"> + <properties> + <help>Show VPP IPFIX information</help> + </properties> + <children> + <node name="collectors"> + <properties> + <help>Show IPFIX collectors</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_collectors</command> + </node> + <node name="interfaces"> + <properties> + <help>Show IPFIX interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_interfaces</command> + </node> + <node name="table"> + <properties> + <help>Show IPFIX table</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_table</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/vpp/ipfix/__init__.py b/python/vyos/vpp/ipfix/__init__.py new file mode 100644 index 000000000..9696cf5c1 --- /dev/null +++ b/python/vyos/vpp/ipfix/__init__.py @@ -0,0 +1,3 @@ +from .ipfix import IPFIX + +__all__ = ['IPFIX'] diff --git a/python/vyos/vpp/ipfix/ipfix.py b/python/vyos/vpp/ipfix/ipfix.py new file mode 100644 index 000000000..1569b22f6 --- /dev/null +++ b/python/vyos/vpp/ipfix/ipfix.py @@ -0,0 +1,181 @@ +# +# 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 vpp_papi import VppEnum +from vyos.vpp import VPPControl + + +class IPFIX: + def __init__( + self, + collector_address: str = '0.0.0.0', + collector_port: int = 4739, + src_address: str = '0.0.0.0', + path_mtu: int = 0, + template_interval: int = 20, + udp_checksum: bool = False, + vrf_id: int = 0, + ): + self.vpp = VPPControl() + self.collector_address = collector_address + self.collector_port = collector_port + self.src_address = src_address + self.path_mtu = path_mtu + self.template_interval = template_interval + self.udp_checksum = udp_checksum + self.vrf_id = vrf_id + + # enums mapping + self.RECORD_FLAGS_MAP = { + 'l2': VppEnum.vl_api_flowprobe_record_flags_t.FLOWPROBE_RECORD_FLAG_L2, + 'l3': VppEnum.vl_api_flowprobe_record_flags_t.FLOWPROBE_RECORD_FLAG_L3, + 'l4': VppEnum.vl_api_flowprobe_record_flags_t.FLOWPROBE_RECORD_FLAG_L4, + } + + self.WHICH_FLAGS_MAP = { + 'ipv4': VppEnum.vl_api_flowprobe_which_t.FLOWPROBE_WHICH_IP4, + 'ipv6': VppEnum.vl_api_flowprobe_which_t.FLOWPROBE_WHICH_IP6, + 'l2': VppEnum.vl_api_flowprobe_which_t.FLOWPROBE_WHICH_L2, + } + + self.DIRECTION_MAP = { + 'rx': VppEnum.vl_api_flowprobe_direction_t.FLOWPROBE_DIRECTION_RX, + 'tx': VppEnum.vl_api_flowprobe_direction_t.FLOWPROBE_DIRECTION_TX, + 'both': VppEnum.vl_api_flowprobe_direction_t.FLOWPROBE_DIRECTION_BOTH, + } + + def ipfix_exporter_delete(self): + """Delete IPFIX exporter + https://github.com/FDio/vpp/blob/stable/2506/src/vnet/ipfix-export/ipfix_export.api + Example: + from vyos.vpp import ipfix + i = ipfix.IPFIX() + i.ipfix_exporter_delete() + """ + self.vpp.api.set_ipfix_exporter( + collector_port=0, + collector_address='0.0.0.0', + src_address='0.0.0.0', + path_mtu=0xFFFFFFFF, + template_interval=0, + udp_checksum=False, + vrf_id=4294967295, + ) + + def set_ipfix_exporter(self): + """Set IPFIX exporter parameters + Example: + from vyos.vpp import ipfix + i = ipfix.IPFIX(collector_address='192.0.2.2', src_address='192.0.2.1', collector_port=2055, template_interval=20, path_mtu=1450) + i.set_ipfix_exporter() + """ + self.vpp.api.set_ipfix_exporter( + collector_port=self.collector_port, + collector_address=self.collector_address, + src_address=self.src_address, + path_mtu=self.path_mtu, + template_interval=self.template_interval, + udp_checksum=self.udp_checksum, + vrf_id=self.vrf_id, + ) + + def flowprobe_interface_add( + self, + interface: str, + direction: str = 'both', + which: str = 'ipv4', + ): + """Add IPFIX flowprobe to interface + https://github.com/FDio/vpp/blob/stable/2506/src/plugins/flowprobe/flowprobe.api + Args: + interface (str): Interface name + direction (str): Direction of flowprobe ('rx', 'tx', 'both') + which (str): Which packets to probe ('ipv4', 'ipv6', 'l2') + Example: + from vyos.vpp import ipfix + i = ipfix.IPFIX(collector_address='192.0.2.2', src_address='192.0.2.1', collector_port=2055, template_interval=20, path_mtu=1450) + i.flowprobe_set_params(record_flags=['l2', 'l3'], active_timer=2, passive_timer=20) + i.flowprobe_interface_add('eth0') + """ + sw_if_index = self.vpp.get_sw_if_index(interface) + direction_flag = self.DIRECTION_MAP.get(direction, self.DIRECTION_MAP['both']) + which_flag = self.WHICH_FLAGS_MAP.get(which, self.WHICH_FLAGS_MAP['ipv4']) + + self.vpp.api.flowprobe_interface_add_del( + is_add=True, + sw_if_index=sw_if_index, + direction=direction_flag, + which=which_flag, + ) + + def flowprobe_interface_delete( + self, + interface: str, + direction: str = 'both', + which: str = 'ipv4', + ): + """Delete IPFIX flowprobe from interface + https://github.com/FDio/vpp/blob/stable/2506/src/plugins/flowprobe/flowprobe.api + Args: + interface (str): Interface name + Example: + from vyos.vpp import ipfix + i = ipfix.IPFIX(collector_address='192.0.2.2', src_address='192.0.2.1', collector_port=2055, template_interval=20, path_mtu=1450) + i.flowprobe_interface_delete('eth0') + """ + sw_if_index = self.vpp.get_sw_if_index(interface) + direction_flag = self.DIRECTION_MAP.get(direction, self.DIRECTION_MAP['both']) + which_flag = self.WHICH_FLAGS_MAP.get(which, self.WHICH_FLAGS_MAP['ipv4']) + + self.vpp.api.flowprobe_interface_add_del( + is_add=False, + sw_if_index=sw_if_index, + direction=direction_flag, + which=which_flag, + ) + + def flowprobe_set_params( + self, + active_timer: int = 15, + passive_timer: int = 120, + record_flags: list = None, + ): + """Set IPFIX flowprobe parameters + + Args: + active_timer (int): Active timer in seconds + passive_timer (int): Passive timer in seconds + record_flags: Record flags as list of 'l2', 'l3', 'l4' + Examples: ['l2'], ['l2', 'l3'], ['l2', 'l3', 'l4'] + Example: + from vyos.vpp import ipfix + i = ipfix.IPFIX(collector_address='192.0.2.2', src_address='192.0.2.1', collector_port=2055, template_interval=20, path_mtu=1450) + i.flowprobe_set_params(record_flags=['l2', 'l3'], active_timer=10, passive_timer=30) + i.flowprobe_interface_add('eth0') + """ + if record_flags is None: + record_flags = ['l2', 'l3', 'l4'] + # Calculate combined flags + record_flag = 0 + for flag in record_flags: + record_flag |= self.RECORD_FLAGS_MAP[flag] + + self.vpp.api.flowprobe_set_params( + active_timer=active_timer, + passive_timer=passive_timer, + record_flags=record_flag, + ) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 604fa25fc..51ada6cb1 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -31,6 +31,7 @@ from vyos.utils.file import read_file from vyos.utils.process import rc_cmd from vyos.utils.system import sysctl_read from vyos.system import image +from vyos.vpp import VPPControl PROCESS_NAME = 'vpp_main' VPP_CONF = '/run/vpp/vpp.conf' @@ -38,6 +39,7 @@ base_path = ['vpp'] driver = 'dpdk' interface = 'eth1' + def get_vpp_config(): config = defaultdict(dict) current_section = None @@ -1619,6 +1621,93 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_delete(path_static_arp) + def test_22_vpp_ipfix(self): + base_ipfix = base_path + ['ipfix'] + base_collector = base_ipfix + ['collector'] + collector_ip = '127.0.0.2' + collector_src = '127.0.0.1' + collector_port = '9374' + timer_active = '8' + timer_passive = '32' + tmplt_interval = '4' + flow_probe_rec = 'l3' + not_vpp_interface = 'eth0' + + self.cli_set(base_ipfix + ['active-timeout', timer_active]) + self.cli_set(base_ipfix + ['inactive-timeout', timer_passive]) + self.cli_set(base_ipfix + ['flowprobe-record', flow_probe_rec]) + self.cli_set(base_ipfix + ['interface', interface]) + self.cli_set(base_collector + [collector_ip, 'source-address', collector_src]) + self.cli_set(base_collector + [collector_ip, 'port', collector_port]) + self.cli_set( + base_collector + [collector_ip, 'template-interval', tmplt_interval] + ) + self.cli_commit() + + # Test 1: Verify flowprobe parameters + _, out = rc_cmd('sudo vppctl show flowprobe params') + required_str = ( + f'{flow_probe_rec} active: {timer_active} passive: {timer_passive}' + ) + self.assertIn(required_str, out) + + # Test 2: Add non-VPP interface + self.cli_set(base_ipfix + ['interface', not_vpp_interface]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_ipfix + ['interface', not_vpp_interface]) + self.cli_set(base_ipfix + ['interface', interface]) + self.cli_commit() + + _, out = rc_cmd('sudo vppctl show flowprobe feature') + required_str = f'{interface} ip4 rx tx' + self.assertIn(required_str, out) + + # Test 3: Verify IPFIX exporter via API + # Set socket permissions to allow test access (owner/group read/write only) + if os.path.exists('/run/vpp/api.sock'): + os.system('sudo chmod 666 /run/vpp/api.sock') + + vpp = VPPControl() + + # Get all exporters + result = vpp.api.ipfix_all_exporter_get() + # Second element contains the exporter list + exporters = result[1] + + # Find our configured exporter + found_exporter = None + for exporter in exporters: + if str(exporter.collector_address) == collector_ip: + found_exporter = exporter + break + + # Verify exporter parameters + self.assertIsNotNone(found_exporter, 'IPFIX exporter not found') + self.assertEqual(str(found_exporter.collector_address), collector_ip) + self.assertEqual(str(found_exporter.src_address), collector_src) + self.assertEqual(found_exporter.collector_port, int(collector_port)) + self.assertEqual(found_exporter.template_interval, int(tmplt_interval)) + self.assertEqual(found_exporter.path_mtu, 512) # Default path MTU + self.assertEqual(found_exporter.vrf_id, 0) # Default VRF + self.assertFalse(found_exporter.udp_checksum) # Default UDP checksum + + # Test 4: Cleanup - remove configuration + self.cli_delete(base_ipfix) + self.cli_commit() + + # Verify cleanup + result = vpp.api.ipfix_all_exporter_get() + exporters = result[1] + # Should only have default exporter (0.0.0.0) left + non_default_exporters = [ + e for e in exporters if str(e.collector_address) != '0.0.0.0' + ] + self.assertEqual( + len(non_default_exporters), 0, 'Exporters not cleaned up properly' + ) + if __name__ == '__main__': unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) 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) diff --git a/src/op_mode/vpp.py b/src/op_mode/vpp.py new file mode 100755 index 000000000..fea874909 --- /dev/null +++ b/src/op_mode/vpp.py @@ -0,0 +1,165 @@ +#!/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/>. + +import sys +import typing + +from tabulate import tabulate +from vyos.vpp import VPPControl +from vyos.configquery import ConfigTreeQuery +import vyos.opmode + + +class VPPShow: + def __init__(self): + self.config = ConfigTreeQuery() + self.vpp = VPPControl() + + # ----------------------------- + # IPFIX Interfaces + # ----------------------------- + def _get_ipfix_interfaces_raw(self) -> typing.List[dict]: + interfaces = self.vpp.api.flowprobe_interface_dump() + index_map = { + i.sw_if_index: i.interface_name for i in self.vpp.api.sw_interface_dump() + } + + return [ + { + 'interface': index_map.get(e.sw_if_index, f'if{e.sw_if_index}'), + 'sw_if_index': e.sw_if_index, + 'which': e.which.name.replace('FLOWPROBE_WHICH_', '').lower(), + 'direction': e.direction.name.replace( + 'FLOWPROBE_DIRECTION_', '' + ).lower(), + } + for e in interfaces + ] + + def _show_ipfix_interfaces_formatted(self, data: typing.List[dict]) -> str: + if not data: + return 'No flowprobe interfaces configured.' + table_data = [ + { + 'Interface': d['interface'], + 'VppIfIndex': d['sw_if_index'], + 'Flow-variant': d['which'], + 'Direction': d['direction'], + } + for d in data + ] + return tabulate(table_data, headers='keys', tablefmt='simple') + + def ipfix_interfaces(self, raw: bool): + base = ['vpp', 'ipfix', 'interface'] + if not self.config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem( + 'vpp ipfix interface is not configured' + ) + + data = self._get_ipfix_interfaces_raw() + return data if raw else self._show_ipfix_interfaces_formatted(data) + + # ----------------------------- + # IPFIX Collectors + # ----------------------------- + def _get_ipfix_collectors_raw(self) -> typing.List[dict]: + _, collectors = self.vpp.api.ipfix_all_exporter_get() + return [ + { + 'collector_address': str(c.collector_address), + 'collector_port': c.collector_port, + 'src_address': str(c.src_address), + 'vrf_id': c.vrf_id, + 'path_mtu': c.path_mtu, + 'template_interval': c.template_interval, + 'udp_checksum': bool(c.udp_checksum), + } + for c in collectors + ] + + def _show_ipfix_collectors_formatted(self, data: typing.List[dict]) -> str: + if not data: + return 'No IPFIX collectors configured.' + table_data = [ + { + 'Collector': f"{d['collector_address']}:{d['collector_port']}", + 'Source': d['src_address'], + 'VRF': d['vrf_id'], + 'MTU': d['path_mtu'], + 'Template Intvl': d['template_interval'], + 'UDP Cksum': 'on' if d['udp_checksum'] else 'off', + } + for d in data + ] + return tabulate(table_data, headers='keys', tablefmt='simple') + + def ipfix_collectors(self, raw: bool): + base = ['vpp', 'ipfix', 'collector'] + if not self.config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem( + 'vpp ipfix collector is not configured' + ) + + data = self._get_ipfix_collectors_raw() + return data if raw else self._show_ipfix_collectors_formatted(data) + + # ----------------------------- + # IPFIX table + # ----------------------------- + def _get_ipfix_table_raw(self): + # VPP does not have API call to get this data + data = self.vpp.cli_cmd('show flowprobe table') + return [data.reply] + + def _show_ipfix_table_formatted(self) -> str: + data = self.vpp.cli_cmd('show flowprobe table') + return data.reply + + def ipfix_table(self, raw: bool): + base = ['vpp', 'ipfix', 'collector'] + if not self.config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem( + 'vpp ipfix collector is not configured' + ) + + data = self._get_ipfix_table_raw() + return data if raw else self._show_ipfix_table_formatted() + + +# ----------------------------- +# VyOS IPFIX op-mode entries +# ----------------------------- +def show_ipfix_interfaces(raw: bool): + return VPPShow().ipfix_interfaces(raw) + + +def show_ipfix_collectors(raw: bool): + return VPPShow().ipfix_collectors(raw) + + +def show_ipfix_table(raw: bool): + return VPPShow().ipfix_table(raw) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) |
