Merge pull request #19 from pimvanpelt/sflow

sFlow plugin support
This commit is contained in:
Pim van Pelt
2024-10-28 16:19:53 +01:00
committed by GitHub
8 changed files with 163 additions and 2 deletions

View File

@ -314,14 +314,15 @@ exist as a PHY in VPP (ie. `HundredGigabitEthernet12/0/0`) or as a specified `Bo
target interface. target interface.
* ***state***: An optional string that configures the link admin state, either `up` or `down`. * ***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'. 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. `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 * ***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. 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 * ***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 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. 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 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 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 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 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). 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
```

View File

@ -40,6 +40,7 @@ from .vxlan_tunnel import validate_vxlan_tunnels
from .tap import validate_taps from .tap import validate_taps
from .prefixlist import validate_prefixlists from .prefixlist import validate_prefixlists
from .acl import validate_acls from .acl import validate_acls
from .sflow import validate_sflow
class IPInterfaceWithPrefixLength(validators.Validator): class IPInterfaceWithPrefixLength(validators.Validator):
@ -94,6 +95,7 @@ class Validator:
validate_taps, validate_taps,
validate_prefixlists, validate_prefixlists,
validate_acls, validate_acls,
validate_sflow,
] ]
def validate(self, yaml): def validate(self, yaml):

30
vppcfg/config/sflow.py Normal file
View File

@ -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

View File

@ -10,10 +10,12 @@ interfaces:
device-type: dpdk device-type: dpdk
mtu: 9000 mtu: 9000
description: "LAG #1" description: "LAG #1"
sflow: true
GigabitEthernet3/0/1: GigabitEthernet3/0/1:
device-type: dpdk device-type: dpdk
mtu: 9000 mtu: 9000
description: "LAG #2" description: "LAG #2"
sflow: false
HundredGigabitEthernet12/0/0: HundredGigabitEthernet12/0/0:
device-type: dpdk device-type: dpdk
@ -163,3 +165,8 @@ acls:
icmp-code: any icmp-code: any
- description: "Deny any IPv4 or IPv6" - description: "Deny any IPv4 or IPv6"
action: deny action: deny
sflow:
header-bytes: 128
polling-interval: 30
sampling-rate: 1000

View File

@ -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) 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) 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) acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=56),required=False)
sflow: include('sflow',required=False)
--- ---
vxlan: vxlan:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)
@ -57,6 +58,7 @@ interface:
state: enum('up', 'down', required=False) state: enum('up', 'down', required=False)
mpls: bool(required=False) mpls: bool(required=False)
device-type: enum('dpdk', required=False) device-type: enum('dpdk', required=False)
sflow: bool(required=False)
--- ---
sub-interface: sub-interface:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)
@ -113,3 +115,8 @@ acl-term:
acl: acl:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)
terms: list(include('acl-term'), min=1, max=100, required=True) 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)

View File

@ -67,6 +67,7 @@ class Dumper(VPPApi):
"taps": {}, "taps": {},
"prefixlists": {}, "prefixlists": {},
"acls": {}, "acls": {},
"sflow": {},
} }
for idx, bond_iface in self.cache["bondethernets"].items(): for idx, bond_iface in self.cache["bondethernets"].items():
bond = {"description": ""} bond = {"description": ""}
@ -141,6 +142,8 @@ class Dumper(VPPApi):
i["addresses"] = self.cache["interface_addresses"][ i["addresses"] = self.cache["interface_addresses"][
iface.sw_if_index 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"]: if iface.sw_if_index in self.cache["l2xcs"]:
l2xc = self.cache["l2xcs"][iface.sw_if_index] l2xc = self.cache["l2xcs"][iface.sw_if_index]
i["l2xc"] = self.cache["interfaces"][ i["l2xc"] = self.cache["interfaces"][
@ -353,4 +356,9 @@ class Dumper(VPPApi):
config["acls"][aclname] = config_acl 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 return config

View File

@ -967,6 +967,9 @@ class Reconciler:
if not self.__sync_mpls_state(): if not self.__sync_mpls_state():
self.logger.warning("Could not sync interface MPLS state in VPP") self.logger.warning("Could not sync interface MPLS state in VPP")
ret = False 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(): if not self.__sync_admin_state():
self.logger.warning("Could not sync interface adminstate in VPP") self.logger.warning("Could not sync interface adminstate in VPP")
ret = False ret = False
@ -1311,6 +1314,55 @@ class Reconciler:
ret = False ret = False
return ret 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): def __sync_mpls_state(self):
"""Synchronize the VPP Dataplane configuration for interface and loopback MPLS state""" """Synchronize the VPP Dataplane configuration for interface and loopback MPLS state"""
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces( for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(

View File

@ -130,6 +130,8 @@ class VPPApi:
"taps": {}, "taps": {},
"acls": {}, "acls": {},
"acl_tags": {}, "acl_tags": {},
"interface_sflow": {},
"sflow": {},
} }
return True return True
@ -415,6 +417,33 @@ class VPPApi:
for tap in api_response: for tap in api_response:
self.cache["taps"][tap.sw_if_index] = tap 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 self.cache_read = True
return self.cache_read return self.cache_read