Add an optional configuration file

A simple convenience configfile can provide a mapping between VPP
interface names, Linux Control Plane interface names, and descriptions.
An example:

```
interfaces:
  "TenGigabitEthernet6/0/0":
    description: "Infra: xsw0.chrma0:2"
    lcp: "xe1-0"
  "TenGigabitEthernet6/0/0.3102":
    description: "Infra: QinQ to Solnet for Daedalean"
    lcp: "xe1-0.3102"
  "TenGigabitEthernet6/0/0.310211":
    description: "Cust: Daedalean IP Transit"
    lcp: "xe1-0.3102.11"
```

This configuration file is completely optional. If the `-c` flag is
empty, or it's set but the file does not exist, the Agent will simply
enumerate all interfaces, and set the `ifAlias` OID to the same value
as the `ifName`. However, if the config file is read, it will change
the behavior as follows:

*  Any `tapNN` interface names from VPP will be matched to their PHY by
   looking up their Linux Control Plane interface. The `ifName` field
   will be rewritten to the _LIP_ `host-if`. For example, `tap3` above
   will become `xe1-0` while `tap3.310211` will become `xe1-0.3102.11`.
*  The `ifAlias` OID for a PHY will be set to the `description` field.
*  The `ifAlias` OID for a TAP will be set to the string `LCP: `
   followed by its PHY `ifName`. For example, `xe1-0.3102.11` will
    become `LCP TenGigabitEthernet6/0/0.310211 (tap9)`
This commit is contained in:
Pim van Pelt
2022-02-27 22:58:03 +00:00
parent 80190bf2d0
commit c319ef576d
6 changed files with 167 additions and 8 deletions

View File

@ -36,12 +36,42 @@ optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-a ADDRESS Location of the SNMPd agent (unix-path or host:port), default localhost:705 -a ADDRESS Location of the SNMPd agent (unix-path or host:port), default localhost:705
-p PERIOD Period to poll VPP, default 30 (seconds) -p PERIOD Period to poll VPP, default 30 (seconds)
-c CONFIG Optional YAML configuration file, default empty
-d Enable debug, default False -d Enable debug, default False
## Install ## Install
sudo cp dist/vpp-snmp-agent /usr/sbin/ sudo cp dist/vpp-snmp-agent /usr/sbin/
``` ```
## Configuration file
A simple convenience configfile can provide a mapping between VPP interface names, Linux Control Plane
interface names, and descriptions. An example:
```
interfaces:
"TenGigabitEthernet6/0/0":
description: "Infra: xsw0.chrma0:2"
lcp: "xe1-0"
"TenGigabitEthernet6/0/0.3102":
description: "Infra: QinQ to Solnet for Daedalean"
lcp: "xe1-0.3102"
"TenGigabitEthernet6/0/0.310211":
description: "Cust: Daedalean IP Transit"
lcp: "xe1-0.3102.11"
```
This configuration file is completely optional. If the `-c` flag is empty, or it's set but the file does
not exist, the Agent will simply enumerate all interfaces, and set the `ifAlias` OID to the same value as
the `ifName`. However, if the config file is read, it will change the behavior as follows:
* Any `tapNN` interface names from VPP will be matched to their PHY by looking up their Linux Control Plane
interface. The `ifName` field will be rewritten to the _LIP_ `host-if`. For example, `tap3` above will
become `xe1-0` while `tap3.310211` will become `xe1-0.3102.11`.
* The `ifAlias` OID for a PHY will be set to the `description` field.
* The `ifAlias` OID for a TAP will be set to the string `LCP: ` followed by its PHY `ifName`. For example,
`xe1-0.3102.11` will become `LCP: TenGigabitEthernet6/0/0.310211 (tap9)`
## SNMPd config ## SNMPd config
First, configure the snmpd to accept agentx connections by adding (at least) the following First, configure the snmpd to accept agentx connections by adding (at least) the following

View File

@ -97,7 +97,7 @@ class Network():
self.socket.send(pdu.encode()) self.socket.send(pdu.encode())
def recv_pdu(self): def recv_pdu(self):
buf = self.socket.recv(8192) buf = self.socket.recv(100000)
if not buf: return None if not buf: return None
pdu = PDU() pdu = PDU()
pdu.decode(buf) pdu.decode(buf)

View File

@ -4,6 +4,7 @@
from vppstats import VPPStats from vppstats import VPPStats
from vppapi import VPPApi from vppapi import VPPApi
import sys import sys
import yaml
import agentx import agentx
try: try:
@ -12,10 +13,39 @@ except ImportError:
print("ERROR: install argparse manually: sudo pip install argparse") print("ERROR: install argparse manually: sudo pip install argparse")
sys.exit(2) sys.exit(2)
def get_phy_by_sw_if_index(ifaces, sw_if_index):
try:
for k,v in ifaces.items():
if v.sw_if_index == sw_if_index:
return v
except:
pass
return None
def get_lcp_by_host_sw_if_index(lcp, host_sw_if_index):
try:
for k,v in lcp.items():
if v['host_sw_if_index'] == host_sw_if_index:
return v
except:
pass
return None
class MyAgent(agentx.Agent): class MyAgent(agentx.Agent):
def setup(self): def setup(self):
global vppstat, vpp, logger global vppstat, vpp, logger, args
self.config = None
if args.config:
try:
with open(args.config, "r") as f:
self.logger.info("Loading configfile %s" % args.config)
self.config = yaml.load(f, Loader = yaml.FullLoader)
self.logger.debug("Config: %s" % self.config)
except:
self.logger.error("Couldn't read config from %s" % args.config)
self.logger.info("Connecting to VPP Stats Segment") self.logger.info("Connecting to VPP Stats Segment")
vppstat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2) vppstat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2)
@ -41,15 +71,45 @@ class MyAgent(agentx.Agent):
ds = agentx.DataSet() ds = agentx.DataSet()
ifaces = vpp.get_ifaces() ifaces = vpp.get_ifaces()
self.logger.debug("%d VPP interfaces retrieved" % len(ifaces)) lcp = vpp.get_lcp()
self.logger.debug("%d VPP Stats interfaces retrieved" % len(vppstat['/if/names'])) num_ifaces=len(ifaces)
num_vppstat=len(vppstat['/if/names'])
num_lcp=len(lcp)
self.logger.debug("LCP: %s" % (lcp))
self.logger.debug("Retrieved Interfaces: vppapi=%d vppstats=%d lcp=%d" % (num_ifaces, num_vppstat, num_lcp))
if num_ifaces != num_vppstat:
self.logger.error("Disconnecting due to error: vppapi=%d vppstats=%d" % (num_ifaces, num_vppstat))
vpp.disconnect()
vppstat.disconnect()
return False
for i in range(len(vppstat['/if/names'])): for i in range(len(vppstat['/if/names'])):
ifname = vppstat['/if/names'][i] ifname = vppstat['/if/names'][i]
idx = 1000+i idx = 1000+i
ds.set('1.3.6.1.2.1.2.2.1.1.%u' % (idx), 'int', idx) ds.set('1.3.6.1.2.1.2.2.1.1.%u' % (idx), 'int', idx)
ds.set('1.3.6.1.2.1.2.2.1.2.%u' % (idx), 'str', ifname)
ifName=ifname
ifAlias=None
try:
if self.config and ifname.startswith('tap'):
host_sw_if_index = ifaces[ifname].sw_if_index
lip = get_lcp_by_host_sw_if_index(lcp, host_sw_if_index)
if lip:
phy = get_phy_by_sw_if_index(ifaces, lip['phy_sw_if_index'])
self.logger.debug("LIP: %s PHY: %s" % (lip, phy))
if phy:
ifName = self.config['interfaces'][phy.interface_name]['lcp']
self.logger.debug("Setting ifName of %s to '%s'" % (ifname, ifName))
ifAlias = "LCP %s (%s)" % (phy.interface_name,ifname)
self.logger.debug("Setting ifAlias of %s to '%s'" % (ifname, ifAlias))
except:
self.logger.debug("No config entry found for ifname %s" % (ifname))
pass
ds.set('1.3.6.1.2.1.2.2.1.2.%u' % (idx), 'str', ifName)
if ifname.startswith("loop"): if ifname.startswith("loop"):
ds.set('1.3.6.1.2.1.2.2.1.3.%u' % (idx), 'int', 24) # softwareLoopback ds.set('1.3.6.1.2.1.2.2.1.3.%u' % (idx), 'int', 24) # softwareLoopback
@ -114,7 +174,7 @@ class MyAgent(agentx.Agent):
ds.set('1.3.6.1.2.1.2.2.1.19.%u' % (idx), 'u32', vppstat['/if/drops'][:, i].sum() % 2**32) ds.set('1.3.6.1.2.1.2.2.1.19.%u' % (idx), 'u32', vppstat['/if/drops'][:, i].sum() % 2**32)
ds.set('1.3.6.1.2.1.2.2.1.20.%u' % (idx), 'u32', vppstat['/if/tx-error'][:, i].sum() % 2**32) ds.set('1.3.6.1.2.1.2.2.1.20.%u' % (idx), 'u32', vppstat['/if/tx-error'][:, i].sum() % 2**32)
ds.set('1.3.6.1.2.1.31.1.1.1.1.%u' % (idx), 'str', ifname) ds.set('1.3.6.1.2.1.31.1.1.1.1.%u' % (idx), 'str', ifName)
ds.set('1.3.6.1.2.1.31.1.1.1.2.%u' % (idx), 'u32', vppstat['/if/rx-multicast'][:, i].sum_packets() % 2**32) ds.set('1.3.6.1.2.1.31.1.1.1.2.%u' % (idx), 'u32', vppstat['/if/rx-multicast'][:, i].sum_packets() % 2**32)
ds.set('1.3.6.1.2.1.31.1.1.1.3.%u' % (idx), 'u32', vppstat['/if/rx-broadcast'][:, i].sum_packets() % 2**32) ds.set('1.3.6.1.2.1.31.1.1.1.3.%u' % (idx), 'u32', vppstat['/if/rx-broadcast'][:, i].sum_packets() % 2**32)
ds.set('1.3.6.1.2.1.31.1.1.1.4.%u' % (idx), 'u32', vppstat['/if/tx-multicast'][:, i].sum_packets() % 2**32) ds.set('1.3.6.1.2.1.31.1.1.1.4.%u' % (idx), 'u32', vppstat['/if/tx-multicast'][:, i].sum_packets() % 2**32)
@ -141,14 +201,26 @@ class MyAgent(agentx.Agent):
ds.set('1.3.6.1.2.1.31.1.1.1.16.%u' % (idx), 'int', 2) # Hardcode to false(2) ds.set('1.3.6.1.2.1.31.1.1.1.16.%u' % (idx), 'int', 2) # Hardcode to false(2)
ds.set('1.3.6.1.2.1.31.1.1.1.17.%u' % (idx), 'int', 1) # Hardcode to true(1) ds.set('1.3.6.1.2.1.31.1.1.1.17.%u' % (idx), 'int', 1) # Hardcode to true(1)
ds.set('1.3.6.1.2.1.31.1.1.1.18.%u' % (idx), 'str', ifname)
if self.config and not ifAlias:
try:
ifAlias = self.config['interfaces'][ifname]['description']
self.logger.debug("Setting ifAlias of %s to '%s'" % (ifname, ifAlias))
except:
pass
if not ifAlias:
ifAlias = ifname
ds.set('1.3.6.1.2.1.31.1.1.1.18.%u' % (idx), 'str', ifAlias)
ds.set('1.3.6.1.2.1.31.1.1.1.19.%u' % (idx), 'ticks', 0) # Hardcode to Timeticks: (0) 0:00:00.00 ds.set('1.3.6.1.2.1.31.1.1.1.19.%u' % (idx), 'ticks', 0) # Hardcode to Timeticks: (0) 0:00:00.00
return ds return ds
def main(): def main():
global args
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-a', dest='address', default="localhost:705", type=str, help="""Location of the SNMPd agent (unix-path or host:port), default localhost:705""") parser.add_argument('-a', dest='address', default="localhost:705", type=str, help="""Location of the SNMPd agent (unix-path or host:port), default localhost:705""")
parser.add_argument('-p', dest='period', type=int, default=30, help="""Period to poll VPP, default 30 (seconds)""") parser.add_argument('-p', dest='period', type=int, default=30, help="""Period to poll VPP, default 30 (seconds)""")
parser.add_argument('-c', dest='config', type=str, help="""Optional YAML configuration file, default empty""")
parser.add_argument('-d', dest='debug', action='store_true', help="""Enable debug, default False""") parser.add_argument('-d', dest='debug', action='store_true', help="""Enable debug, default False""")
args = parser.parse_args() args = parser.parse_args()

View File

@ -6,7 +6,7 @@ ConditionPathExists=/etc/snmp/snmpd.conf
[Service] [Service]
Type=simple Type=simple
NetworkNamespacePath=/var/run/netns/dataplane NetworkNamespacePath=/var/run/netns/dataplane
ExecStart=/usr/sbin/vpp-snmp-agent -a localhost:705 -p 30 ExecStart=/usr/sbin/vpp-snmp-agent -a localhost:705 -p 30 -c /etc/vpp/vpp-snmp-agent.yaml
User=Debian-snmp User=Debian-snmp
Group=vpp Group=vpp
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID

14
vpp-snmp-agent.yaml Normal file
View File

@ -0,0 +1,14 @@
## Example configuration file for VPP SNMP Agent
##
## See README.md for details on what this (optional) file does.
interfaces:
"GigabitEthernet5/0/0":
description: "Infra: xsw0.chrma0.ipng.ch Te0/2"
lcp: "e0"
"GigabitEthernet5/0/0.3102":
description: "Infra: QinQ to L2 Provider"
lcp: "e0.3102"
"GigabitEthernet5/0/0.310211":
description: "Cust: Downstream IP Transit"
lcp: "e0.3102.11"

View File

@ -7,6 +7,7 @@ from vpp_papi import VPPApiClient
import os import os
import fnmatch import fnmatch
import logging import logging
import socket
class NullHandler(logging.Handler): class NullHandler(logging.Handler):
@ -81,3 +82,45 @@ class VPPApi():
ret[iface.interface_name] = iface ret[iface.interface_name] = iface
return ret return ret
def get_lcp(self):
ret = {}
if not self.connected:
return ret
try:
lcp_list = self.vpp.api.lcp_itf_pair_get()
except Exception as e:
logger.error("VPP communication error, disconnecting", e)
self.vpp.disconnect()
self.connected = False
return ret
if not lcp_list or not lcp_list[1]:
logger.error("Can't get LCP list")
return ret
## TODO(pim) - fix upstream, the indexes are in network byte order and
## the message is messed up. This hack allows for both little endian and
## big endian responses, and will be removed once VPP's LinuxCP is updated
## and rolled out to AS8298
for lcp in lcp_list[1]:
if lcp.phy_sw_if_index > 65535 or lcp.host_sw_if_index > 65535 or lcp.vif_index > 65535:
i = {
'phy_sw_if_index': socket.ntohl(lcp.phy_sw_if_index),
'host_sw_if_index': socket.ntohl(lcp.host_sw_if_index),
'vif_index': socket.ntohl(lcp.vif_index),
'host_if_name': lcp.host_if_name,
'namespace': lcp.namespace
}
else:
i = {
'phy_sw_if_index': lcp.phy_sw_if_index,
'host_sw_if_index': lcp.host_sw_if_index,
'vif_index': lcp.vif_index,
'host_if_name': lcp.host_if_name,
'namespace': lcp.namespace
}
ret[lcp.host_if_name] = i
return ret