From c859738b0fdc0a8ce553e79c34bd7e9d1dc9ba49 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Fri, 25 Oct 2024 14:04:16 +0200 Subject: [PATCH 1/6] Add documentation of intended configuration elements --- docs/config-guide.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/config-guide.md b/docs/config-guide.md index 094f606..1ec893c 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -314,14 +314,15 @@ exist as a PHY in VPP (ie. `HundredGigabitEthernet12/0/0`) or as a specified `Bo target interface. * ***state***: An optional string that configures the link admin state, either `up` or `down`. If it is not specified, the link is considered admin 'up'. -* ***device-type***: An optional interface type in VPP. Currently the only supported vlaue is +* ***device-type***: An optional interface type in VPP. Currently the only supported value is `dpdk`, and it is used to generate correct mock interfaces if the `--novpp` flag is used. * ***mpls***: An optional boolean that configures MPLS on the interface or sub-interface. The default value is `false`, if the field is not specified, which means MPLS will not be enabled. * ***unnumbered***: An interface name from which this (sub-)interface will borrow IPv4 and IPv6 addresses. The interface can be either a loopback, an interface or a sub-interface. if the interface is unnumbered, it can't be L2 and it can't have addresses. - +* ***sflow***: An optional boolean value, when true will enable sFlow collection on this + interface. sFlow collection is only supported on PHY (physical) interfaces. Further, top-level interfaces, that is to say those that do not have an encapsulation, are permitted to have any number of sub-interfaces specified by `subid`, an integer between [0,2G), which further @@ -510,3 +511,28 @@ interfaces: The configuration here is tolerant of either a singleton (a literal string referring to the one ACL that must be applied), or a _list_ of strings to more than one ACL, in which case they will be tested in order (with a first-match return value). + +### sFlow collection + +VPP supports sFlow collection using the `sFlow` plugin. The collection of samples occurs only on +physical interfaces (and will include samples for any sub-interfaces or tunnels created), and is +meant to be enabled on all interfaces (using the `sflow: true` key, see the Interfaces definition +above) that are passing traffic. The defaults in the plugin are sensible and should not need to +be changed. + +The following configuration elements are provided for the plugin: + +* **sample-rate**: Capture 1-in-N packets. Defaults to 10000. A good value is the interface + bitrate divided by 1000, so for GigabitEthernet choose 1000, for TenGigabitEthernet choose + 10000 (the default). +* **polling-interval**: Determines the period of interface byte and packet counter reads. This + information will be added to the sFlow collector data automatically. +* **header-bytes**: The number of bytes taken from the IP packet in the sample. By default, + 128 bytes are taken. This value should not be changed in normal operation. + +``` +sflow: + sample-rate: 10000 + polling-interval: 20 + header-bytes: 128 +``` From 78a6f413aa43d7c1155db9fc1993975a474ffb8d Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 28 Oct 2024 15:49:35 +0100 Subject: [PATCH 2/6] Add sflow schema, validators (currently nothing to do), and dumper --- vppcfg/config/__init__.py | 2 ++ vppcfg/config/sflow.py | 30 ++++++++++++++++++++++++++++++ vppcfg/example.yaml | 7 +++++++ vppcfg/schema.yaml | 7 +++++++ vppcfg/vpp/dumper.py | 30 ++++++++++++++++++------------ vppcfg/vpp/vppapi.py | 29 +++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 vppcfg/config/sflow.py diff --git a/vppcfg/config/__init__.py b/vppcfg/config/__init__.py index d7b469e..ee03d65 100644 --- a/vppcfg/config/__init__.py +++ b/vppcfg/config/__init__.py @@ -40,6 +40,7 @@ from .tap import validate_taps from .prefixlist import validate_prefixlists from .acl import validate_acls +from .sflow import validate_sflow class IPInterfaceWithPrefixLength(validators.Validator): @@ -94,6 +95,7 @@ def __init__(self, schema): validate_taps, validate_prefixlists, validate_acls, + validate_sflow, ] def validate(self, yaml): diff --git a/vppcfg/config/sflow.py b/vppcfg/config/sflow.py new file mode 100644 index 0000000..b220e4b --- /dev/null +++ b/vppcfg/config/sflow.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024 Pim van Pelt +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" A vppcfg configuration module that validates sflow config """ +import logging + + +def validate_sflow(yaml): + """Validate the semantics of all YAML 'sflow' config entries""" + result = True + msgs = [] + logger = logging.getLogger("vppcfg.config") + logger.addHandler(logging.NullHandler()) + + if not "sflow" in yaml: + return result, msgs + + ## NOTE(pim): Nothing to validate. sflow config values are all + ## integers and enforced by yamale. + return result, msgs diff --git a/vppcfg/example.yaml b/vppcfg/example.yaml index d9bcbff..88be079 100644 --- a/vppcfg/example.yaml +++ b/vppcfg/example.yaml @@ -10,10 +10,12 @@ interfaces: device-type: dpdk mtu: 9000 description: "LAG #1" + sflow: true GigabitEthernet3/0/1: device-type: dpdk mtu: 9000 description: "LAG #2" + sflow: false HundredGigabitEthernet12/0/0: device-type: dpdk @@ -163,3 +165,8 @@ acls: icmp-code: any - description: "Deny any IPv4 or IPv6" action: deny + +sflow: + header-bytes: 128 + polling-interval: 30 + sample-rate: 1000 diff --git a/vppcfg/schema.yaml b/vppcfg/schema.yaml index 7e60b2f..0a95dae 100644 --- a/vppcfg/schema.yaml +++ b/vppcfg/schema.yaml @@ -6,6 +6,7 @@ vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),requir taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False) prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=64),required=False) acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=56),required=False) +sflow: include('sflow',required=False) --- vxlan: description: str(exclude='\'"',len=64,required=False) @@ -57,6 +58,7 @@ interface: state: enum('up', 'down', required=False) mpls: bool(required=False) device-type: enum('dpdk', required=False) + sflow: bool(required=False) --- sub-interface: description: str(exclude='\'"',len=64,required=False) @@ -113,3 +115,8 @@ acl-term: acl: description: str(exclude='\'"',len=64,required=False) terms: list(include('acl-term'), min=1, max=100, required=True) +--- +sflow: + header-bytes: int(min=1,max=256,required=False) + polling-interval: int(min=5,max=600,required=False) + sample-rate: int(min=100,max=1000000,required=False) diff --git a/vppcfg/vpp/dumper.py b/vppcfg/vpp/dumper.py index 7463f3a..fa08d09 100644 --- a/vppcfg/vpp/dumper.py +++ b/vppcfg/vpp/dumper.py @@ -67,6 +67,7 @@ def cache_to_config(self): "taps": {}, "prefixlists": {}, "acls": {}, + "sflow": {}, } for idx, bond_iface in self.cache["bondethernets"].items(): bond = {"description": ""} @@ -301,9 +302,9 @@ def cache_to_config(self): acl_rule.srcport_or_icmptype_first ) else: - config_term["icmp-type"] = ( - f"{acl_rule.srcport_or_icmptype_first}-{maxval}" - ) + config_term[ + "icmp-type" + ] = f"{acl_rule.srcport_or_icmptype_first}-{maxval}" maxval = acl_rule.dstport_or_icmpcode_last if maxval > 255: @@ -316,9 +317,9 @@ def cache_to_config(self): acl_rule.dstport_or_icmpcode_first ) else: - config_term["icmp-code"] = ( - f"{acl_rule.dstport_or_icmpcode_first}-{maxval}" - ) + config_term[ + "icmp-code" + ] = f"{acl_rule.dstport_or_icmpcode_first}-{maxval}" elif acl_rule.proto in [6, 17]: if acl_rule.proto == 6: config_term["protocol"] = "tcp" @@ -332,9 +333,9 @@ def cache_to_config(self): acl_rule.srcport_or_icmptype_first ) else: - config_term["source-port"] = ( - f"{acl_rule.srcport_or_icmptype_first}-{acl_rule.srcport_or_icmptype_last}" - ) + config_term[ + "source-port" + ] = f"{acl_rule.srcport_or_icmptype_first}-{acl_rule.srcport_or_icmptype_last}" if ( acl_rule.dstport_or_icmpcode_first == acl_rule.dstport_or_icmpcode_last @@ -343,9 +344,9 @@ def cache_to_config(self): acl_rule.dstport_or_icmpcode_first ) else: - config_term["destination-port"] = ( - f"{acl_rule.dstport_or_icmpcode_first}-{acl_rule.dstport_or_icmpcode_last}" - ) + config_term[ + "destination-port" + ] = f"{acl_rule.dstport_or_icmpcode_first}-{acl_rule.dstport_or_icmpcode_last}" else: config_term["protocol"] = int(acl_rule.proto) @@ -353,4 +354,9 @@ def cache_to_config(self): config["acls"][aclname] = config_acl + config["sflow"] = self.cache["sflow"] + for hw_if_index in self.cache["interface_sflow"]: + vpp_iface = self.cache["interfaces"][hw_if_index] + config["interfaces"][vpp_iface.interface_name]["sflow"] = True + return config diff --git a/vppcfg/vpp/vppapi.py b/vppcfg/vpp/vppapi.py index 660667a..631deb1 100644 --- a/vppcfg/vpp/vppapi.py +++ b/vppcfg/vpp/vppapi.py @@ -130,6 +130,8 @@ def cache_clear(self): "taps": {}, "acls": {}, "acl_tags": {}, + "interface_sflow": {}, + "sflow": {}, } return True @@ -415,6 +417,33 @@ def readconfig(self): for tap in api_response: self.cache["taps"][tap.sw_if_index] = tap + try: + self.logger.debug("Retrieving sFlow") + + api_response = self.vpp.api.sflow_sampling_rate_get() + if api_response: + self.cache["sflow"]["sampling-rate"] = api_response.sampling_N + api_response = self.vpp.api.sflow_polling_interval_get() + if api_response: + self.cache["sflow"]["polling-interval"] = api_response.polling_S + api_response = self.vpp.api.sflow_header_bytes_get() + if api_response: + self.cache["sflow"]["header-bytes"] = api_response.header_B + + api_response = self.vpp.api.sflow_interface_dump() + for iface in api_response: + self.cache["interface_sflow"][iface.hw_if_index] = True + except AttributeError as err: + self.logger.warning(f"sFlow API not found - missing plugin: {err}") + + self.logger.debug("Retrieving interface Unnumbered state") + api_response = self.vpp.api.ip_unnumbered_dump() + for iface in api_response: + self.cache["interface_unnumbered"][iface.sw_if_index] = iface.ip_sw_if_index + + self.logger.debug("Retrieving bondethernets") + api_response = self.vpp.api.sw_bond_interface_dump() + self.cache_read = True return self.cache_read From 80058fceed2150485dbf93a78443a7b225f79359 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 28 Oct 2024 16:12:55 +0100 Subject: [PATCH 3/6] The correct configuration option is 'sampling-rate' --- vppcfg/example.yaml | 2 +- vppcfg/schema.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vppcfg/example.yaml b/vppcfg/example.yaml index 88be079..f690bbc 100644 --- a/vppcfg/example.yaml +++ b/vppcfg/example.yaml @@ -169,4 +169,4 @@ acls: sflow: header-bytes: 128 polling-interval: 30 - sample-rate: 1000 + sampling-rate: 1000 diff --git a/vppcfg/schema.yaml b/vppcfg/schema.yaml index 0a95dae..c905d4e 100644 --- a/vppcfg/schema.yaml +++ b/vppcfg/schema.yaml @@ -119,4 +119,4 @@ acl: sflow: header-bytes: int(min=1,max=256,required=False) polling-interval: int(min=5,max=600,required=False) - sample-rate: int(min=100,max=1000000,required=False) + sampling-rate: int(min=100,max=1000000,required=False) From 4ba8c59fd824ffc2c2a372b8af505db351b6ac60 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 28 Oct 2024 16:13:34 +0100 Subject: [PATCH 4/6] Add planner for sFlow --- vppcfg/vpp/reconciler.py | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/vppcfg/vpp/reconciler.py b/vppcfg/vpp/reconciler.py index 6797044..500424f 100644 --- a/vppcfg/vpp/reconciler.py +++ b/vppcfg/vpp/reconciler.py @@ -967,6 +967,9 @@ def sync(self): if not self.__sync_mpls_state(): self.logger.warning("Could not sync interface MPLS state in VPP") ret = False + if not self.__sync_sflow_state(): + self.logger.warning("Could not sync interface sFlow state in VPP") + ret = False if not self.__sync_admin_state(): self.logger.warning("Could not sync interface adminstate in VPP") ret = False @@ -1311,6 +1314,55 @@ def __sync_mtu(self): ret = False return ret + def __sync_sflow_state(self): + """Synchronize the VPP Dataplane configuration and phy sFlow state""" + + if "sflow" in self.cfg and self.vpp.cache["sflow"]: + if "header-bytes" in self.cfg["sflow"]: + if ( + self.vpp.cache["sflow"]["header-bytes"] + != self.cfg["sflow"]["header-bytes"] + ): + cli = f"sflow header-bytes {self.cfg['sflow']['header-bytes']}" + self.cli["sync"].append(cli) + if "polling-interval" in self.cfg["sflow"]: + if ( + self.vpp.cache["sflow"]["polling-interval"] + != self.cfg["sflow"]["polling-interval"] + ): + cli = f"sflow polling-interval {self.cfg['sflow']['polling-interval']}" + self.cli["sync"].append(cli) + if "sampling-rate" in self.cfg["sflow"]: + if ( + self.vpp.cache["sflow"]["sampling-rate"] + != self.cfg["sflow"]["sampling-rate"] + ): + cli = f"sflow sampling-rate {self.cfg['sflow']['sampling-rate']}" + self.cli["sync"].append(cli) + + for ifname in interface.get_interfaces(self.cfg): + vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname) + + try: + config_sflow = config_iface["sflow"] + except KeyError: + config_sflow = False + + vpp_sflow = False + if vpp_ifname in self.vpp.cache["interface_names"]: + hw_if_index = self.vpp.cache["interface_names"][vpp_ifname] + try: + vpp_sflow = self.vpp.cache["interface_sflow"][hw_if_index] + except KeyError: + pass + if vpp_sflow != config_sflow: + if config_sflow: + cli = f"sflow enable {vpp_ifname}" + else: + cli = f"sflow enable-disable {vpp_ifname} disable" + self.cli["sync"].append(cli) + return True + def __sync_mpls_state(self): """Synchronize the VPP Dataplane configuration for interface and loopback MPLS state""" for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces( From 8f7c65d8ca60ca02392e496dd3af8440770fb787 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 28 Oct 2024 16:16:10 +0100 Subject: [PATCH 5/6] bugfix: also dump 'mpls' state on interfaces --- vppcfg/vpp/dumper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vppcfg/vpp/dumper.py b/vppcfg/vpp/dumper.py index fa08d09..2b0262f 100644 --- a/vppcfg/vpp/dumper.py +++ b/vppcfg/vpp/dumper.py @@ -142,6 +142,8 @@ def cache_to_config(self): i["addresses"] = self.cache["interface_addresses"][ iface.sw_if_index ] + if iface.sw_if_index in self.cache["interface_mpls"]: + i["mpls"] = self.cache["interface_mpls"][iface.sw_if_index] if iface.sw_if_index in self.cache["l2xcs"]: l2xc = self.cache["l2xcs"][iface.sw_if_index] i["l2xc"] = self.cache["interfaces"][ From ab5f1e43c07ff451eb9e46ac1f9c372213bb570a Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 28 Oct 2024 16:18:37 +0100 Subject: [PATCH 6/6] format with black 24.10.0 --- vppcfg/vpp/dumper.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/vppcfg/vpp/dumper.py b/vppcfg/vpp/dumper.py index 2b0262f..c08832a 100644 --- a/vppcfg/vpp/dumper.py +++ b/vppcfg/vpp/dumper.py @@ -304,9 +304,9 @@ def cache_to_config(self): acl_rule.srcport_or_icmptype_first ) else: - config_term[ - "icmp-type" - ] = f"{acl_rule.srcport_or_icmptype_first}-{maxval}" + config_term["icmp-type"] = ( + f"{acl_rule.srcport_or_icmptype_first}-{maxval}" + ) maxval = acl_rule.dstport_or_icmpcode_last if maxval > 255: @@ -319,9 +319,9 @@ def cache_to_config(self): acl_rule.dstport_or_icmpcode_first ) else: - config_term[ - "icmp-code" - ] = f"{acl_rule.dstport_or_icmpcode_first}-{maxval}" + config_term["icmp-code"] = ( + f"{acl_rule.dstport_or_icmpcode_first}-{maxval}" + ) elif acl_rule.proto in [6, 17]: if acl_rule.proto == 6: config_term["protocol"] = "tcp" @@ -335,9 +335,9 @@ def cache_to_config(self): acl_rule.srcport_or_icmptype_first ) else: - config_term[ - "source-port" - ] = f"{acl_rule.srcport_or_icmptype_first}-{acl_rule.srcport_or_icmptype_last}" + config_term["source-port"] = ( + f"{acl_rule.srcport_or_icmptype_first}-{acl_rule.srcport_or_icmptype_last}" + ) if ( acl_rule.dstport_or_icmpcode_first == acl_rule.dstport_or_icmpcode_last @@ -346,9 +346,9 @@ def cache_to_config(self): acl_rule.dstport_or_icmpcode_first ) else: - config_term[ - "destination-port" - ] = f"{acl_rule.dstport_or_icmpcode_first}-{acl_rule.dstport_or_icmpcode_last}" + config_term["destination-port"] = ( + f"{acl_rule.dstport_or_icmpcode_first}-{acl_rule.dstport_or_icmpcode_last}" + ) else: config_term["protocol"] = int(acl_rule.proto)