Contents
I once wanted something simple enough to monitor hosts with SNMP in my company's network. I tried Cacti Nagios, FAN, Centreon but could never be satisfied due to the inherent level of complexity.
Fact I was looking for something as simple as sysfs or procfs in Linux to apply to SNMP. And I found nothing. So I decided to code something. But as I like challenges I went for Python. I must confess it was not the easiest challenge I ever took on my own. I started writing a Python FUSE file system using PySNMP and FusePy by Terrence Honles as the base blocks. As a result I spent a considerable amount of time getting concepts such as decorators, descriptors, duck typing and python idioms through my brain.
This project has been challenging to the point that even though I had a clear idea what I wanted to achieve it took me months to be satisfied with what I wrote. I even trashed my code a couple of times until it was exactly like I wanted it to be. All in all this new Python experience has been an immersing fun. I think I've now come to where I wanted to go as a first and major step.
SNMP is a long-standing network protocol that has evolved and is — as of this writing — at version 3. It not only supports reading values from SNMP agents (aka hosts) but can also send commands and trigger actions. Let's summarize some interesting features.
Access to agents is made possible by defining a community name. The default community name for reading is public and the default name for writing is private. Version 3 of the protocol also supports password or cryptographic authentication.
SNMP defines Object IDentifiers (OID), which are represented using ASN.1 dotted-numeric notation, just like an IPv4 address but theoretically with an unlimited number of dot-separated digits. For instance .1.3.6.1.2.1.1.1.0 represents SNMPv2-MIB::sysDescr.0 the descripting part of an SNMP agent (a machine, a host).
snmpget -v2c -c Community -Ofn 10.0.0.3 1.3.6.1.2.1.1.1.0
SNMPv2-MIB::sysDescr.0 = STRING: Linux myhost.mynet.local 2.6.32-042stab062.2 #1 SMP Wed Oct 10 17:54:01 MSD 2012 i686
These identifiers are defined in a Management Information Base (MIB), which is actually a text file containing the textual definition of a group of related Object Identifiers. There are a number of generic MIBs — such as SNMPv2-MIB, IF-MIB, IP-MIB, HOST-RESOURCES-MIB — but also vendor-specific MIBs, such as Cisco, Microsoft, Xerox, Alcatel...
A Management Information Base can be browsed and typically contains atomic OID or tables. Tables are just a set of indexed rows, each with a fixed number of ordered fields. Command line tools to retrieve values and tables are snmpget and snmptable.
Here are some common table OIDs:
| Description | MIB | Name | OID |
|---|---|---|---|
| Interface table | IF-MIB | ifTable | 1.3.6.1.2.1.2.2 |
| IPv4 address table | IP-MIB | ipAddrTable | 1.3.6.1.2.1.4.20 |
| Storage table | HOST-RESOURCES-MIB | hrStorageTable | 1.3.6.1.2.1.25.2.3 |
There are also MIBs for installed software, processor load and many more.
Note that SNMP tables lists tables by column, not by rows. I.e. the returned items start with all the available indices then all the available column values, in field order.
Not all indicators need to be monitored at the same rate. Typically network traffic needs to be monitored more often than disk space, for example, respectively every 20 seconds and every hour. The description of a machine is also supposed not to change once registered in a network. A machine's load average might be watched every 5 or 10 minutes. A standard monitoring application has to define an unlimited number of schedulers.
SNMP bindings are available in every language, which is a good thing. The drawback is that every SNMP monitoring application needs to make SNMP calls and implement another network application layer. The latter can be factored out.
Monitoring can't be without graphs. Note that Round Robin Databases (RRD) supports feeding databases at a rate that is higher than defined in the database, which then does all the job and averages the samples for the defined period. It spares the task of defining specific schedulers.
The idea is to navigate a file system instead of having to code SNMP calls in every single monitoring application. The project mounts a caching loopback filesystem into /run/snmp by default. It intercepts system calls like mkdir() and open() to initialize the directory tree for a host and update values with SNMP calls whenever values are outdated.
python snmpfs.py public
The cached file system can be found in /var/cache/snmpfs under a directory that is named after the SNMP community, public in the above example. When pysnmpfs is run, a directory with the community name is created if it doesn't exist and that directory is then mounted as a looback file system under /run/snmp. In this example, the visible (mounted) cached tree is located in /var/cache/snmpfs/public.
Monitoring a host is as simple as creating a directory under /run/snmp. The directory must be named after the host to monitor:
cd /run/snmp
mkdir myhost.mynet.local
The system then automatically populates the directory with files and subdirectories as fetched from SNMP agents:
ls -l
total 20
-rw-r----- 1 snmp snmp 42 nov 6 09:56 admin
-rw-r----- 1 snmp snmp 89 nov 6 09:56 descr
-rw-r----- 1 snmp snmp 22 nov 6 09:56 hostname
-rw-r----- 1 snmp snmp 24 nov 6 09:56 location
drwxr-x--- 11 snmp snmp 4096 nov 6 09:56 net
The subdirectory tree is described in self-registering plugins.
Plugins are user-defined classes grouped into extensions, which are in fact Python scripts. Plugins are stored in directory /extensions.
cd /usr/local/share/bin
ls -l extensions
-rw-r--r-- 1 root root 1296 nov 1 18:12 host.py
-rw-r--r-- 1 root root 1204 nov 1 18:12 __init__.py
-rw-r--r-- 1 root root 2002 nov 3 14:37 interface.py
Values fetched by plugins are cached in files, which are organized in a directory hierarchy. Every plugin defines its own directory tree. In the above example, files admin, descr, hostname and location are defined in the host plugin (extensions/host.py) while directory net is defined in the interface plugin (extensions/interface.py).
A plugin may retrieve either an SNMP table or a batch of atomic values, i.e. a GET request with multiple values. The project is shipped with two plugins: one for interface data (IF-MIB::ifTable), the other for agent-specific descriptors (from SNMPv2-MIB).
Every plugin defines its own Time To Live (TTL). It is only when a cached file is too old that an SNMP request is sent to the monitored host, which minimizes network trafic and stretches it to when strictly necessary: there's no network traffic if no file is read. The same scheduler may then be programmed to read all the values from the snmpfs tree and feed RRD.
Note that GNU/Linux crons do not allow periods of less than one minute.
Here is the host plugin:
from plugin import Plugin
class HostPlugin(Plugin):
TTL = 86400 # One day
def __init__(self):
batch = Plugin.Batch('SNMPv2-MIB')
super(self.__class__, self).__init__(command=batch, nodes={
'descr': batch.select(('sysDescr', 0)),
'admin': batch.select(('sysContact', 0)),
'hostname': batch.select(('sysName', 0)),
'location': batch.select(('sysLocation', 0))
})
This plugin fetches SNMPv2-MIB::sysDescr.0 (the registered host description), SNMPv2-MIB::sysContact.0 (the registered contact), SNMPv2-MIB::sysName.0 (the registered host name(*) on the network) and SNMPv2-MIB::sysLocation.0 (the registered location of the host).
(*) Note that not all hosts set sysName.0 to their registered name on the network, which Windows and GNU/Linux hosts typically do.
The cached directory allows writing only from the community root directory, e.g. /var/cache/snmpfs/public. Subsequent directories and files can only be read by the snmp group members and, of course, the snmp user itself. Access by any other user is denied. Since pysnmpfs is a running process, all files and directories are automatically owned by the snmp user, regardless of the currently logged user session.