diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json
index 0f8d6521cd..54136f7790 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 5d3f4a249d..d1f23bfb05 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 edfb5b36da..6cec6ae813 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 e10a3409d8..749fb3fbfd 100644
--- a/interface-definitions/vpp.xml.in
+++ b/interface-definitions/vpp.xml.in
@@ -346,6 +346,182 @@
+
+
+ IP Flow Information Export (IPFIX) configuration
+ 322
+
+
+
+
+ Collector IP address
+
+ ipv4
+ IPv4 server to export IPFIX
+
+
+ ipv6
+ IPv6 server to export IPFIX
+
+
+
+
+
+
+ #include
+
+ 4739
+
+
+
+ Path MTU
+
+ u32:68-1450
+ Bytes
+
+
+
+
+
+ 512
+
+ #include
+
+
+ Interval in seconds
+
+ u32:1-300
+ Seconds
+
+
+
+
+
+ 20
+
+
+
+ Allow UDP checksum
+
+
+
+
+
+
+
+ Interface
+
+
+
+
+ txt
+ Interface name
+
+
+
+
+
+ Flow direction
+
+ rx tx both
+
+
+ rx
+ Rx direction
+
+
+ tx
+ Tx direction
+
+
+ both
+ Rx and Tx direction
+
+
+ (rx|tx|both)
+
+
+ both
+
+
+
+ Flow variant
+
+ l2 ipv4 ipv6
+
+
+ l2
+ L2
+
+
+ _ipv4
+ IPv4
+
+
+ _ipv6
+ IPv6
+
+
+ (l2|ipv4|ipv6)
+
+
+ ipv4
+
+
+
+
+
+ Flow activity timeout
+
+ u32:0-2147483647
+ Active flow export timeout (seconds)
+
+
+
+
+
+ 15
+
+
+
+ Flow inactivity timeout
+
+ u32:0-2147483647
+ Inactive flow export timeout (seconds)
+
+
+
+
+
+ 120
+
+
+
+ Flow record layers
+
+ l2 l3 l4
+
+
+ l2
+ Include level 2 information
+
+
+ l3
+ Include level 3 information
+
+
+ l4
+ Include level 4 information
+
+
+ (l2|l3|l4)
+
+
+
+ l3
+
+
+
VPP settings
diff --git a/op-mode-definitions/vpp_ipfix.xml.in b/op-mode-definitions/vpp_ipfix.xml.in
new file mode 100644
index 0000000000..90e860e2dd
--- /dev/null
+++ b/op-mode-definitions/vpp_ipfix.xml.in
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+ Show VPP IPFIX information
+
+
+
+
+ Show IPFIX collectors
+
+ sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_collectors
+
+
+
+ Show IPFIX interfaces
+
+ sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_interfaces
+
+
+
+ Show IPFIX table
+
+ sudo ${vyos_op_scripts_dir}/vpp.py show_ipfix_table
+
+
+
+
+
+
+
+
diff --git a/python/vyos/vpp/ipfix/__init__.py b/python/vyos/vpp/ipfix/__init__.py
new file mode 100644
index 0000000000..9696cf5c11
--- /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 0000000000..1569b22f6e
--- /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 604fa25fcb..51ada6cb1a 100755
--- a/smoketest/scripts/cli/test_vpp.py
+++ b/smoketest/scripts/cli/test_vpp.py
@@ -31,6 +31,7 @@
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 @@
driver = 'dpdk'
interface = 'eth1'
+
def get_vpp_config():
config = defaultdict(dict)
current_section = None
@@ -1619,6 +1621,93 @@ def test_21_static_arp(self):
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 bd7ff173f6..abb68847eb 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 0000000000..a6659257c4
--- /dev/null
+++ b/src/conf_mode/vpp_ipfix.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# 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 .
+#
+
+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 0000000000..fea8749096
--- /dev/null
+++ b/src/op_mode/vpp.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# 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 .
+
+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)