Snimpy: SNMP & Python
Vincent Bernat
While quite old fashioned, SNMP is still a ubiquitous protocol supported by most network devices. It comes handy to expose various metrics, like network interface counters, to be gathered for the purpose of monitoring. It can also be used to retrieve and modify devices’ configuration.
Variables exposed by SNMP agents (servers) are organized inside a Management Information Base (MIB) which is a hierarchical database.1 Each entry is identified by an OID. Querying a specific OID allows a manager (client) to get the value of an associated variable.
For example, one common MIB-module is IF-MIB
, defined in RFC 2863. It contains objects used to manage network interfaces. One of
them is ifTable
whose rows are representing agent’s logical network
interfaces. Each row will expose the interface name, characteristics
and various associated counters.
ifIndex | ifDescr | ifPhysAddress | ifOperStatus | ifOutOctets |
---|---|---|---|---|
1 | lo | up | 545721741 | |
2 | eth0 | 0:18:f3:3:4e:4 | up | 78875421 |
3 | eth1 | 0:18:f3:3:4e:5 | down | 0 |
ifTable
is indexed by its first column ifIndex
. If you want to get
the operational status of the second interface, you need to query
IF-MIB::ifOperStatus.2
which is translated to OID
.1.3.6.1.2.1.2.2.1.8.2
using information provided by the MIB definition.
Scripting SNMP#
An SNMP agent can deliver a lot of interesting information:
- interface counters with IF-MIB
- IP addresses with IP-MIB
- routing tables with IP-FORWARD-MIB
- inventory with ENTITY-MIB
- neighbors with LLDP-MIB
You can gather these information manually with tools like snmpget
and snmpwalk
:
$ snmpwalk -v 2c -c public localhost IF-MIB::ifDescr IF-MIB::ifDescr.1 = STRING: lo IF-MIB::ifDescr.2 = STRING: eth0 IF-MIB::ifDescr.3 = STRING: eth1
However, building robust scripts with them is quite challenging. For example, if you wanted to get the descriptions of all active interfaces as well as the total number of octets transmitted, you could do something like that:
#!/bin/sh set -e host="${1:-localhost}" community="${2:-public}" args="-v2c -c $community $host" for idx in $(snmpwalk -Ov -OQ $args IF-MIB::ifIndex); do descr=$(snmpget -Ov -OQ $args IF-MIB::ifDescr.$idx) oper=$(snmpget -Ov -OQ $args IF-MIB::ifOperStatus.$idx) in=$(snmpget -Ov -OQ $args IF-MIB::ifInOctets.$idx) out=$(snmpget -Ov -OQ $args IF-MIB::ifOutOctets.$idx) [ x"$descr" != x"lo" ] || continue [ x"$oper" = x"up" ] || continue echo $descr $in $out done
Hopefully, SNMP bindings in various languages are pretty common. For example, Net-SNMP ships with a Python binding:
import argparse import netsnmp parser = argparse.ArgumentParser() parser.add_argument( "host", default="localhost", nargs="?", help="Agent to retrieve variables from") parser.add_argument( "community", default="public", nargs="?", help="Community to query the agent") options = parser.parse_args() args = { "Version": 2, "DestHost": options.host, "Community": options.community } for idx in netsnmp.snmpwalk(netsnmp.Varbind("IF-MIB::ifIndex"), **args): descr, oper, cin, cout = netsnmp.snmpget( netsnmp.Varbind("IF-MIB::ifDescr", idx), netsnmp.Varbind("IF-MIB::ifOperStatus", idx), netsnmp.Varbind("IF-MIB::ifInOctets", idx), netsnmp.Varbind("IF-MIB::ifOutOctets", idx), **args) assert(descr is not None and cin is not None and cout is not None) # ❶ if descr == "lo": continue if oper != "1": # ❷ continue print("{} {} {}".format(descr, cin, cout))
This binding is quite primitive and has several drawbacks:
- It exports everything as strings. See ❷.
- Error handling is just deficient. If you mispell something, like
a variable name, you’ll get
snmp_build: unknown failure
on the standard error. No exception. If a variable does not exist, you’ll getNone
instead. See ❶.
This inability to sanely handle failures makes this binding quite
dangerous to use in scripts. Imagine making important modifications on
the basis of the returned values. If you forget to check against
None
, your script may cause havoc!
Snimpy#
Because I didn’t find any reliable Python binding for SNMP, I decided to write Snimpy with two goals in mind:
- Leverage information contained in MIBs to provide a pythonic interface.
- Any error condition should raise an exception.
Here is how the previous script could be written:
#!/usr/bin/env snimpy import argparse parser = argparse.ArgumentParser() parser.add_argument( "host", default="localhost", nargs="?", help="Agent to retrieve variables from") parser.add_argument( "community", default="public", nargs="?", help="Community to query the agent") options = parser.parse_args() m = M(options.host, options.community, 2) load("IF-MIB") for idx in m.ifDescr: if m.ifDescr[idx] == "lo": continue if m.ifOperStatus[idx] != "up": continue print("{} {} {}".format( m.ifDescr[idx], m.ifInOctets[idx], m.ifOutOctets[idx]))
You can also use a list comprehension:
load("IF-MIB") print("\n".join([ "{} {} {}".format( m.ifDescr[idx], m.ifInOctets[idx], m.ifOutOctets[idx]) for idx in m.ifDescr if m.ifDescr[idx] != "lo" and m.ifOperStatus[idx] == "up" ]))
Here is another simple example to get the routing database from the agent:
load("IP-FORWARD-MIB") m=M("localhost", "public", 2) routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("{:>15s}/{:<15s} via {:<15s} src {:<15s}".format( net, netmask, routes[x], src))
IP-FORWARD-MIB::ipCidrRouteNextHop
is a more complex table with a
compound index. Despite this, querying the table still seems natural.
Have a look at Snimpy’s documentation for more information. Under the hood, SNMP requests are handled by PySNMP and MIB parsing is done with libsmi.2 Snimpy supports both Python 2, Python 3 and Pypy.
-
A MIB is defined using a subset of ASN.1 called SMI. However, it is not uncommon to refer to the definition as a MIB too. ↩︎
-
Unfortunately, there is currently no robust SMI parser written in pure Python. For example, PySNMP relies onPySNMP now relies on PySMI. Snimpy uses a custom CFFI wrapper around libsmi. ↩︎smidump
which comes with libsmi.