Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sFlow plugin support #19

Merged
merged 6 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions docs/config-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
2 changes: 2 additions & 0 deletions vppcfg/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -94,6 +95,7 @@ def __init__(self, schema):
validate_taps,
validate_prefixlists,
validate_acls,
validate_sflow,
]

def validate(self, yaml):
Expand Down
30 changes: 30 additions & 0 deletions vppcfg/config/sflow.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions vppcfg/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions vppcfg/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
8 changes: 8 additions & 0 deletions vppcfg/vpp/dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def cache_to_config(self):
"taps": {},
"prefixlists": {},
"acls": {},
"sflow": {},
}
for idx, bond_iface in self.cache["bondethernets"].items():
bond = {"description": ""}
Expand Down Expand Up @@ -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"][
Expand Down Expand Up @@ -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
52 changes: 52 additions & 0 deletions vppcfg/vpp/reconciler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
29 changes: 29 additions & 0 deletions vppcfg/vpp/vppapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ def cache_clear(self):
"taps": {},
"acls": {},
"acl_tags": {},
"interface_sflow": {},
"sflow": {},
}
return True

Expand Down Expand Up @@ -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

Expand Down