From 4c08b0694c65f1bb434538980a44a94e5ae15d24 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Fri, 17 Oct 2025 15:21:41 +0300 Subject: [PATCH] T7938: VPP: Rewrite sFlow implementation Execute commands for vpp sflow with API calls. Use values for polling interval and sampling rate from 'system sflow'. Add op-mode command --- data/config-mode-dependencies/vyos-1x.json | 3 + .../include/version/vpp-version.xml.i | 2 +- interface-definitions/vpp.xml.in | 39 +++++++++-- op-mode-definitions/vpp_sflow.xml.in | 17 +++++ python/vyos/vpp/sflow/__init__.py | 3 + python/vyos/vpp/sflow/sflow.py | 49 ++++++++++++++ smoketest/scripts/cli/test_vpp.py | 37 +++++++++++ src/conf_mode/system_sflow.py | 44 ++++++++++--- src/conf_mode/vpp_sflow.py | 64 +++++++++---------- src/migration-scripts/vpp/2-to-3 | 30 +++++++++ 10 files changed, 239 insertions(+), 49 deletions(-) create mode 100644 op-mode-definitions/vpp_sflow.xml.in create mode 100644 python/vyos/vpp/sflow/__init__.py create mode 100644 python/vyos/vpp/sflow/sflow.py create mode 100644 src/migration-scripts/vpp/2-to-3 diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index d186c52bb5..56538883e6 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -80,5 +80,8 @@ "system_option": { "ip_ipv6": ["system_ip", "system_ipv6"], "sysctl": ["system_sysctl"] + }, + "system_sflow": { + "vpp_sflow": ["vpp_sflow"] } } diff --git a/interface-definitions/include/version/vpp-version.xml.i b/interface-definitions/include/version/vpp-version.xml.i index 205e87916d..b92e9a21aa 100644 --- a/interface-definitions/include/version/vpp-version.xml.i +++ b/interface-definitions/include/version/vpp-version.xml.i @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 911c6f6682..96e7358d8e 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -952,14 +952,45 @@ - + - sFlow sample rate + sFlow maximum packet-header length + + 64 96 128 160 192 224 256 + + + 64 + 64 bytes + + + 96 + 96 bytes + + + 128 + 128 bytes + + + 160 + 160 bytes + + + 192 + 192 bytes + + + 224 + 224 bytes + - u32 - sFlow sample rate + 256 + 256 bytes + + (64|96|128|160|192|224|256) + + 128 diff --git a/op-mode-definitions/vpp_sflow.xml.in b/op-mode-definitions/vpp_sflow.xml.in new file mode 100644 index 0000000000..012e867a60 --- /dev/null +++ b/op-mode-definitions/vpp_sflow.xml.in @@ -0,0 +1,17 @@ + + + + + + + + + Show VPP sFlow information + + bash -c 'if cli-shell-api existsActive vpp sflow; then sudo vppctl show sflow; else echo "vpp sflow is not configured"; fi' + + + + + + diff --git a/python/vyos/vpp/sflow/__init__.py b/python/vyos/vpp/sflow/__init__.py new file mode 100644 index 0000000000..8d27be75a7 --- /dev/null +++ b/python/vyos/vpp/sflow/__init__.py @@ -0,0 +1,3 @@ +from .sflow import SFlow + +__all__ = ['SFlow'] diff --git a/python/vyos/vpp/sflow/sflow.py b/python/vyos/vpp/sflow/sflow.py new file mode 100644 index 0000000000..7677d32b32 --- /dev/null +++ b/python/vyos/vpp/sflow/sflow.py @@ -0,0 +1,49 @@ +# +# Copyright (C) VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class SFlow: + def __init__(self): + self.vpp = VPPControl() + + def enable_sflow(self, interface): + """Enable sFlow on interface""" + self.vpp.api.sflow_enable_disable( + enable_disable=True, + sw_if_index=self.vpp.get_sw_if_index(interface), + ) + + def disable_sflow(self, interface): + """Disable sFlow on interface""" + self.vpp.api.sflow_enable_disable( + enable_disable=False, + sw_if_index=self.vpp.get_sw_if_index(interface), + ) + + def set_sampling_rate(self, sample_rate): + """Set sFlow sampling-rate""" + self.vpp.api.sflow_sampling_rate(sampling_N=sample_rate) + + def set_polling_interval(self, interval): + """Set sFlow polling interval""" + self.vpp.api.sflow_polling_interval(polling_S=interval) + + def set_header_bytes(self, header_bytes): + """Set sFlow maximum header length in bytes""" + self.vpp.api.sflow_header_bytes(header_B=header_bytes) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 6b8f3061df..d0b9458fb4 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -1423,10 +1423,17 @@ def test_16_vpp_nat(self): def test_17_vpp_sflow(self): base_sflow = ['system', 'sflow'] + sampling_rate = '1500' + polling_interval = '55' + header_bytes = '256' + iface_2 = 'eth0' self.cli_set(base_path + ['sflow', 'interface', interface]) + self.cli_set(base_path + ['sflow', 'header-bytes', header_bytes]) self.cli_set(base_sflow + ['interface', interface]) self.cli_set(base_sflow + ['server', '127.0.0.1']) + self.cli_set(base_sflow + ['sampling-rate', sampling_rate]) + self.cli_set(base_sflow + ['polling', polling_interval]) self.cli_set(base_sflow + ['vpp']) self.cli_commit() @@ -1434,7 +1441,10 @@ def test_17_vpp_sflow(self): _, out = rc_cmd('sudo vppctl show sflow') expected_entries = ( + f'sflow sampling-rate {sampling_rate}', 'sflow sampling-direction ingress', + f'sflow polling-interval {polling_interval}', + f'sflow header-bytes {header_bytes}', f'sflow enable {interface}', 'interfaces enabled: 1', ) @@ -1442,9 +1452,36 @@ def test_17_vpp_sflow(self): for expected_entry in expected_entries: self.assertIn(expected_entry, out) + self.cli_set(base_path + ['settings', 'interface', iface_2, 'driver', driver]) + self.cli_set(base_path + ['sflow', 'interface', iface_2]) + + self.cli_commit() + + # Check sFlow + _, out = rc_cmd('sudo vppctl show sflow') + + expected_entries = ( + f'sflow enable {interface}', + f'sflow enable {iface_2}', + 'interfaces enabled: 2', + ) + + for expected_entry in expected_entries: + self.assertIn(expected_entry, out) + + # cannot delete system sFlow configuration if VPP sFlow is configured + # expect raise ConfigError self.cli_delete(base_sflow) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path + ['sflow']) self.cli_commit() + # Check interfaces are deleted from VPP sFlow + _, out = rc_cmd('sudo vppctl show sflow') + self.assertIn('interfaces enabled: 0', out) + def test_18_resource_limits(self): max_map_count = '100000' shmmax = '55555555555555' diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index c5fe5641f1..d54801ecf7 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -19,12 +19,14 @@ from sys import exit from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.configverify import verify_vrf from vyos.template import render from vyos.utils.process import call from vyos.utils.network import is_addr_assigned from vyos import ConfigError from vyos import airbag + airbag.enable() hsflowd_conf_path = '/run/sflow/hsflowd.conf' @@ -38,17 +40,37 @@ def get_config(config=None): else: conf = Config() base = ['system', 'sflow'] + + vpp_sflow = conf.exists(['vpp', 'sflow']) + if not conf.exists(base): - return None + return { + 'remove': True, + 'vpp_sflow': vpp_sflow, + } - sflow = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) + sflow = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True + ) + + sflow.update({'vpp_sflow': vpp_sflow}) + + if vpp_sflow: + set_dependents('vpp_sflow', conf) return sflow + def verify(sflow): - if not sflow: + # Check if "vpp" flag could be deleted from configuration + if sflow.get('vpp_sflow'): + if 'vpp' not in sflow or 'remove' in sflow: + raise ConfigError( + 'sFlow is still configured in VPP. ' + 'Please remove sFlow configuration from VPP before proceeding.' + ) + + if 'remove' in sflow: return None # Check if configured sflow agent-address exist in the system @@ -62,8 +84,7 @@ def verify(sflow): # Check if at least one interface is configured # Skip this check if VPP is enabled if 'interface' not in sflow and 'vpp' not in sflow: - raise ConfigError( - 'sFlow requires at least one interface to be configured!') + raise ConfigError('sFlow requires at least one interface to be configured!') # Check if at least one server is configured if 'server' not in sflow: @@ -72,8 +93,9 @@ def verify(sflow): verify_vrf(sflow) return None + def generate(sflow): - if not sflow: + if 'remove' in sflow: return None render(hsflowd_conf_path, 'sflow/hsflowd.conf.j2', sflow) @@ -81,8 +103,9 @@ def generate(sflow): # Reload systemd manager configuration call('systemctl daemon-reload') + def apply(sflow): - if not sflow: + if 'remove' in sflow: # Stop flow-accounting daemon and remove configuration file call(f'systemctl stop {systemd_service}') if os.path.exists(hsflowd_conf_path): @@ -92,6 +115,9 @@ def apply(sflow): # Start/reload flow-accounting daemon call(f'systemctl restart {systemd_service}') + call_dependents() + + if __name__ == '__main__': try: config = get_config() diff --git a/src/conf_mode/vpp_sflow.py b/src/conf_mode/vpp_sflow.py index 7da4c24d61..df9d0365f3 100644 --- a/src/conf_mode/vpp_sflow.py +++ b/src/conf_mode/vpp_sflow.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# # Copyright VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify @@ -16,7 +18,7 @@ from vyos import ConfigError from vyos.config import Config from vyos.vpp.utils import cli_ifaces_list -from vyos.vpp import VPPControl +from vyos.vpp.sflow import SFlow def get_config(config=None) -> dict: @@ -52,21 +54,22 @@ def get_config(config=None) -> dict: key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, + with_recursive_defaults=True, ) if system_sflow: config['system_sflow'] = system_sflow - if not config: + 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)}) - if effective_config: - config.update({'effective': effective_config}) - return config @@ -76,7 +79,7 @@ def verify(config): # Check if interface section exists if 'interface' not in config: - return None + raise ConfigError('Interfaces must be configured for sFlow') # Verify that all interfaces specified exist in VPP for interface in config['interface']: @@ -85,19 +88,10 @@ def verify(config): f'{interface} must be a VPP interface for sFlow monitoring' ) - # Verify sample rate is a positive integer - if 'sample_rate' in config: - try: - sample_rate = int(config['sample_rate']) - if sample_rate <= 0: - raise ConfigError('sFlow sample rate must be a positive integer') - except ValueError: - raise ConfigError('sFlow sample rate must be a valid integer') - # Verify that system sflow has enable-vpp defined if 'system_sflow' not in config or 'vpp' not in config.get('system_sflow', {}): raise ConfigError( - 'sFlow enable-vpp must be defined under system sflow configuration' + '"sflow vpp" must be defined under system sflow configuration' ) @@ -107,31 +101,31 @@ def generate(config): def apply(config): - # Initialize VPP control API - vpp = VPPControl(attempts=20, interval=500) + s = SFlow() + + # Disable sFlow on deleted interface + for interface in config.get('effective', {}).get('interface', []): + if interface not in config.get('interface', []): + s.disable_sflow(interface) if 'remove' in config: - # Disable sFlow on all interfaces - for interface in config.get('effective', {}).get('interface', []): - vpp.cli_cmd(f'sflow enable-disable {interface} disable') return None - # Configure sample rate if specified - if 'sample_rate' in config: - vpp.cli_cmd(f'sflow sampling-rate {config["sample_rate"]}') + # Configure sample rate + if 'sampling_rate' in config.get('system_sflow', {}): + s.set_sampling_rate(int(config['system_sflow']['sampling_rate'])) + + # Configure polling interval + if 'polling' in config.get('system_sflow', {}): + s.set_polling_interval(int(config['system_sflow']['polling'])) + + # Configure header bytes + if 'header_bytes' in config: + s.set_header_bytes(int(config['header_bytes'])) # Configure interfaces - if 'interface' in config: - # Enable sFlow on specified interfaces - for interface in config['interface']: - vpp.cli_cmd(f'sflow enable-disable {interface}') - - # Disable sFlow on interfaces that were removed from config - effective_interfaces = config.get('effective', {}).get('interface', []) - if effective_interfaces: - for interface in effective_interfaces: - if interface not in config['interface']: - vpp.cli_cmd(f'sflow enable-disable {interface} disable') + for interface in config.get('interface', []): + s.enable_sflow(interface) if __name__ == '__main__': diff --git a/src/migration-scripts/vpp/2-to-3 b/src/migration-scripts/vpp/2-to-3 new file mode 100644 index 0000000000..655862546e --- /dev/null +++ b/src/migration-scripts/vpp/2-to-3 @@ -0,0 +1,30 @@ +# Copyright VyOS maintainers and contributors +# +# 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 . + +# Remove "vpp sflow sample-rate" since it should be automatically inherited +# from "system sflow" to prevent conflicts + +from vyos.configtree import ConfigTree + +base = ['vpp', 'sflow'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + if config.exists(base + ['sample-rate']): + # Delete sample-rate option from sFlow configuration + config.delete(base + ['sample-rate'])