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:
30
README.md
30
README.md
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
14
vpp-snmp-agent.yaml
Normal 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"
|
43
vppapi.py
43
vppapi.py
@ -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
|
||||||
|
Reference in New Issue
Block a user