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 +``` 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..f690bbc 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 + sampling-rate: 1000 diff --git a/vppcfg/schema.yaml b/vppcfg/schema.yaml index 7e60b2f..c905d4e 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) + sampling-rate: int(min=100,max=1000000,required=False) diff --git a/vppcfg/vpp/dumper.py b/vppcfg/vpp/dumper.py index 7463f3a..c08832a 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": ""} @@ -141,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"][ @@ -353,4 +356,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/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( 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