diff --git a/README.md b/README.md index 7d67ea9..f755f0d 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,42 @@ optional arguments: -h, --help show this help message and exit -a ADDRESS Location of the SNMPd agent (unix-path or host:port), default localhost:705 -p PERIOD Period to poll VPP, default 30 (seconds) + -c CONFIG Optional YAML configuration file, default empty -d Enable debug, default False ## Install 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 First, configure the snmpd to accept agentx connections by adding (at least) the following diff --git a/agentx/network.py b/agentx/network.py index 7aef94d..ee72029 100644 --- a/agentx/network.py +++ b/agentx/network.py @@ -97,7 +97,7 @@ class Network(): self.socket.send(pdu.encode()) def recv_pdu(self): - buf = self.socket.recv(8192) + buf = self.socket.recv(100000) if not buf: return None pdu = PDU() pdu.decode(buf) diff --git a/vpp-snmp-agent.py b/vpp-snmp-agent.py index 39aa9fe..92e5bf3 100755 --- a/vpp-snmp-agent.py +++ b/vpp-snmp-agent.py @@ -4,6 +4,7 @@ from vppstats import VPPStats from vppapi import VPPApi import sys +import yaml import agentx try: @@ -12,10 +13,39 @@ except ImportError: print("ERROR: install argparse manually: sudo pip install argparse") 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): 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") vppstat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2) @@ -41,15 +71,45 @@ class MyAgent(agentx.Agent): ds = agentx.DataSet() ifaces = vpp.get_ifaces() - self.logger.debug("%d VPP interfaces retrieved" % len(ifaces)) - self.logger.debug("%d VPP Stats interfaces retrieved" % len(vppstat['/if/names'])) + lcp = vpp.get_lcp() + 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'])): ifname = vppstat['/if/names'][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.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"): 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.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.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) @@ -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.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 return ds def main(): + global args + 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('-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""") args = parser.parse_args() diff --git a/vpp-snmp-agent.service b/vpp-snmp-agent.service index cd7bfee..69a10ab 100644 --- a/vpp-snmp-agent.service +++ b/vpp-snmp-agent.service @@ -6,7 +6,7 @@ ConditionPathExists=/etc/snmp/snmpd.conf [Service] Type=simple 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 Group=vpp ExecReload=/bin/kill -HUP $MAINPID diff --git a/vpp-snmp-agent.yaml b/vpp-snmp-agent.yaml new file mode 100644 index 0000000..ea8a2e0 --- /dev/null +++ b/vpp-snmp-agent.yaml @@ -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" diff --git a/vppapi.py b/vppapi.py index 3a6b884..02546af 100644 --- a/vppapi.py +++ b/vppapi.py @@ -7,6 +7,7 @@ from vpp_papi import VPPApiClient import os import fnmatch import logging +import socket class NullHandler(logging.Handler): @@ -81,3 +82,45 @@ class VPPApi(): ret[iface.interface_name] = iface 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