SNMP is a must-have for SDN. In fact, Simple Network Management Protocol is the best option for controlling devices in software. Even more than that, in-software access is the actual purpose of SNMP. With no surprise, all monitoring systems use SNMP to monitor and control servers and network devices. Having this enormous power that comes from SNMP in a script would be awesome. Thus, this Python SNMP Tutorial explains how to use SNMP in minutes for your script.
Before we start
A couple of words on SNMP
SNMP stands for Simple Network Management Protocol. It is a standard way of communication between a management server and a remote device, the agent. The goal of SNMP is to have the manager understand (or even change) information on the agent. For example, the manager can check which interfaces are up and which are down, or change the hostname of the remote device.
The SNMP agent prepare all the information the manager can read or change in a special table, the MIB. The MIB is a tree-like structure, where each node in the tree will be represented by a number. For example, 1.3.6.1.2.1.1.1
represents the system description. If you are wondering where this chain of numbers came from, this is the whole tree structure! In fact, each number relates to a name. Consequently, we can translate that into a more explicative iso.org.dod.internet.mgmt.mib-2.system.sysDescr
.
We won’t be diving much more into SNMP today. In fact, this Python SNMP tutorial assumes you have an understanding of SNMP. In case you have doubts about that, we got you covered. Just check this SNMP guide.
PySNMP
PySNMP is an open-source module for Python. Unlike telnet or HTTP, Python does not natively implement SNMP. After all, just network and system engineers will need that, not any Python developer on the plant. PySNMP does a great job of covering this lack of native Python. In fact, it allows you to use any version of SNMP, both as an agent or as a manager. Creating an agent means you are creating an application or an appliance, so we won’t check that out today. However, we will check how to use PySNMP to manage a remote device.
As we will see, PySNMP adds a lot of cool stuff to python. With them, it adds some complexity as well. Our goal today is to create a quick python file that can make things simple for you. In that file, you will have all the SNMP operations you need.
Starting our Python SNMP Tutorial
Preparing the environment
First, we need to install PySNMP. We created this tutorial on Python 3.6, but it should work even on old 2.7. If you need to install Python, go with Python 3.x, as 2.7 is now legacy. More on that here.
To start, just install PySNMP with pip. This is the only module we will need for this tutorial.
pip install pysnmp
Now we are ready to start!
Testing our SNMP scripts
There is no joy in preparing our script if we cannot test it. Luckily, we can! There is a cool free online resource that offers a “fake” SNMP agent on the Internet. I am talking about snmplabs.com, and you can point to the fake host demo.snmplabs.com
. However, if you are more into networking you may need something more flexible. If that is the case, you can set up a GNS3 environment as we explained in this article. Then, configure the router with SNMP. For example, a quick way to configure SNMPv2c for read-write is with the following command.
snmp-server community ICTSHORE RW
This allows anyone that knows the password (which is “ICTSHORE”) to access the device in SNMP, Read and Write. Of course, SNMPv2c is not secure, so never use read-write in production. Even more, never use SNMPv2c in production at all! Always opt for SNMPv3.
For this tutorial, we will be testing against a GNS3 router. However, you can do the same tests against snmplabs.com
, you may only need to change MIB Object IDs.
Python SNMP Get
The SNMP Get operation allows you to retrieve the value of an individual object in the MIB. We can also use it to get a list of individual objects, so we can start writing out our get()
function like this:
from pysnmp import hlapi
def get(target, oids, credentials, port=161, engine=hlapi.SnmpEngine(), context=hlapi.ContextData()):
handler = hlapi.getCmd(
engine,
credentials,
hlapi.UdpTransportTarget((target, port)),
context,
*construct_object_types(oids)
)
return fetch(handler, 1)[0]
As you can see, we are leveraging the High-Level API of PySNMP. This is the only part of PySNMP we will use today, as it contains everything we need. Our function is simple: first, it requires a target
(IP or name of the remote device). Then, it requires the list of Object IDs (oids
) we want to get and after that a set of credentials
to authenticate our session. We can also specify a different UDP port
if we want, and use an existing SNMP engine
or custom context
. You may want to use the same engine for all the operations on the same device, as this saves resources. However, this is not required for a simple script, so you can ignore both engine and context.
The function creates a handler for the SNMP session and fetches the information from it. To do that, it relies on two functions we need to create: construct_object_types
and fetch
.
Constructing Object Types
As we said earlier, having more power means having more complexity. This is why the hlapi.getCmd()
function wants some special hlapi.ObjectType
objects, and not a simple list of string OIDs. Thus, our construct_object_type
function creates what PySNMP wants to hear. You can simply copy-and-paste it in your code if you don’t have the time for that. However, this should be a very simple function, take a look:
def construct_object_types(list_of_oids):
object_types = []
for oid in list_of_oids:
object_types.append(hlapi.ObjectType(hlapi.ObjectIdentity(oid)))
return object_types
This returns a list, that we can expand by prepending a *
, as we did in the get()
function. That’s what PySNMP needs.
Fetching data
The fetch()
function is a masterpiece of our Python SMP Tutorial. In fact, we wrote it so that we can re-use it for other Python SNMP functions, like get-bulk. It simply loops on the handler for as many times as we tell it (the count
variable). If it encounter any kind of error, it stops and raises a RuntimeError
. In any other case, it stores the data in a list of dictionaries.
def fetch(handler, count):
result = []
for i in range(count):
try:
error_indication, error_status, error_index, var_binds = next(handler)
if not error_indication and not error_status:
items = {}
for var_bind in var_binds:
items[str(var_bind[0])] = cast(var_bind[1])
result.append(items)
else:
raise RuntimeError('Got SNMP error: {0}'.format(error_indication))
except StopIteration:
break
return result
We have a try ... except StopIteration
construct for a reason. In case the count the user specifies is higher than the number of objects we actually have, we simply stop and return what we got so far. This is the purpose of the construct.
Why do we return a list of dictionaries? In each get operation, we can get multiple object IDs. Thus, each dictionary will have as key the object ID, and as value the value of that OID in the MIB. In case we require multiple OIDs in a single get, we will return a dictionary with multiple keys. Why a list then? With get, you only retrieve the information once. However, as we will see in get bulk, we might want to get the same information many times on different instances. For example, imagine you want to check the errors on all the interfaces: the information is always the errors, but you have multiple instances of it (one per interface). We can visualize it with a list, where each item is a dictionary representing an instance.
Note that the fetch()
function relies on another function we need to create: cast()
. This simply converts the data as received from PySNMP to int, float, or string.
def cast(value):
try:
return int(value)
except (ValueError, TypeError):
try:
return float(value)
except (ValueError, TypeError):
try:
return str(value)
except (ValueError, TypeError):
pass
return value
Providing credentials
The pySNMP authentication system is strong and simple enough. There is no reason to write an additional layer above it, so we can directly use it. Our get() function, as well as the others we need, requires a special authentication object in the credentials variable. This object is different if we use SNMPv2c or SNMPv3.
In the case of SNMPv2c (or lower), we just need to specify the community. We can do that with the CommunityData
object, as below.
hlapi.CommunityData('ICTSHORE'))
Instead, SNMPv3 is trickier. This is because it uses a user with two passwords, and two protocols: for authentication and encryption. Thus, you need to specify the username, authentication password, authentication protocol, encryption password, and encryption protocol. To do that, we need to use the UsmUserData
class. An example would be the one below.
hlapi.UsmUserData('testuser', authKey='authenticationkey', privKey='encryptionkey', authProtocol=hlapi.usmHMACSHAAuthProtocol, privProtocol=hlapi.usmAesCfb128Protocol)
It is simpler than it seems, you just need to know what protocols the remote device is using. However, you may want to check the full UsmUserData documentation.
Getting the hostname!
It is time to test our get()
function. We are going to use it to get the hostname of the device, which is object 1.3.6.1.2.1.1.5.0
. We can simply write this:
print(get('10.0.0.1', ['1.3.6.1.2.1.1.5.0'], hlapi.CommunityData('ICTSHORE')))
This will print something like that:
{'1.3.6.1.2.1.1.5.0': 'R1.sdn.local'}
Of course, R1.sdn.local
is the name of our router. You will get the name of your own router, or what snmplabs provides. You can require more objects if you want, they will all appear in this dictionary.
Note: here we are not getting a list of dictionaries, but just a dictionary. This is intentional, and the get()
function will always behave like that. In fact, since we know that the get function runs only once, it cannot create multiple instances. Therefore, we simply return the first element we got from fetch()
.
Python SNMP Get Bulk
The get_bulk()
function retrieves multiple instances of the same Object ID, for example, one for each interface. It is useful when it comes to working with tables, like the routing table or the interface table. The function is simple and similar to get()
. However, it needs some additional information: which object to start with, and how many instances we want to get. We provide them in start_from
and count
.
def get_bulk(target, oids, credentials, count, start_from=0, port=161,
engine=hlapi.SnmpEngine(), context=hlapi.ContextData()):
handler = hlapi.bulkCmd(
engine,
credentials,
hlapi.UdpTransportTarget((target, port)),
context,
start_from, count,
*construct_object_types(oids)
)
return fetch(handler, count)
Here we return the whole output from fetch()
. We expecting a list of dictionaries, so we don’t want to extract just the first dictionary as we did in get()
.
Python SNMP Get Bulk Auto
This is a cool improvement to get_bulk()
you will appreciate. Imagine you are looping through the interfaces of a device using get_bulk()
. How can you know how many interfaces are there? You need to know that because SNMP wants to know how many times to iterate. You simply can’t know that in advance, but chances are you can find this information using SNMP.
With get_bulk_auto()
, we tell our script to retrieve the count variable from another OID. Thus, instead of specifying a number, we specify an object ID. The function will make a get for that object, and use it as count
.
def get_bulk_auto(target, oids, credentials, count_oid, start_from=0, port=161,
engine=hlapi.SnmpEngine(), context=hlapi.ContextData()):
count = get(target, [count_oid], credentials, port, engine, context)[count_oid]
return get_bulk(target, oids, credentials, count, start_from, port, engine, context)
Using Python SNMP Get Bulk & Get Bulk Auto
Now you can run this code on a device.
its = get_bulk_auto('10.0.0.1', ['1.3.6.1.2.1.2.2.1.2 ', '1.3.6.1.2.1.31.1.1.1.18'], hlapi.CommunityData('ICTSHORE'), '1.3.6.1.2.1.2.1.0')
for it in its:
for k, v in it.items():
print("{0}={1}".format(k, v))
print('')
Our router had 8 interfaces, so this is what we got.
1.3.6.1.2.1.2.2.1.2.1=FastEthernet1/0
1.3.6.1.2.1.31.1.1.1.18.1=
1.3.6.1.2.1.2.2.1.2.2=FastEthernet0/0
1.3.6.1.2.1.31.1.1.1.18.2=
1.3.6.1.2.1.2.2.1.2.3=FastEthernet0/1
1.3.6.1.2.1.31.1.1.1.18.3=Test Desc
1.3.6.1.2.1.2.2.1.2.4=Serial2/0
1.3.6.1.2.1.31.1.1.1.18.4=
1.3.6.1.2.1.2.2.1.2.5=Serial2/1
1.3.6.1.2.1.31.1.1.1.18.5=
1.3.6.1.2.1.2.2.1.2.6=Serial2/2
1.3.6.1.2.1.31.1.1.1.18.6=
1.3.6.1.2.1.2.2.1.2.7=Serial2/3
1.3.6.1.2.1.31.1.1.1.18.7=
1.3.6.1.2.1.2.2.1.2.9=Null0
1.3.6.1.2.1.31.1.1.1.18.9=
Python SNMP Set
Our last function is the set()
function. This allows you to change the value of an OID or a list of OIDs. We implemented it in a very intuitive way: you provide a dictionary of items to change. The key is the object ID, while the value is the new value to set. It works exactly like get()
, but it needs to create a different list of objects.
def set(target, value_pairs, credentials, port=161, engine=hlapi.SnmpEngine(), context=hlapi.ContextData()):
handler = hlapi.setCmd(
engine,
credentials,
hlapi.UdpTransportTarget((target, port)),
context,
*construct_value_pairs(value_pairs)
)
return fetch(handler, 1)[0]
Our construct_value_pairs
function simply converts our input dictionary in a format PySNMP will like.
def construct_value_pairs(list_of_pairs):
pairs = []
for key, value in list_of_pairs.items():
pairs.append(hlapi.ObjectType(hlapi.ObjectIdentity(key), value))
return pairs
Now you can use it, for example to set the hostname:
set('10.0.0.1', {'1.3.6.1.2.1.1.5.0': 'SNMPHost'}, hlapi.CommunityData('ICTSHORE'))
TL;DR Python SNMP made easy
In this tutorial, we saw how to use PySNMP to integrate SNMP in our Python Scripts. While doing so, we populated a file with all the quick functions you need. You can get this file from GitHub and paste it into your project. We suggest you name it quicksnmp.py
.
Then, you can use it like below.
from pysnmp import hlapi
from . import quicksnmp
# Using SNMPv2c, we set the hostname of the remote device to 'SNMPHost'
quicksnmp.set('10.0.0.1', {'1.3.6.1.2.1.1.5.0': 'SNMPHost'}, hlapi.CommunityData('ICTSHORE'))
# Using SNMPv2c, we retrieve the hostname of the remote device
print(get('10.0.0.1', ['1.3.6.1.2.1.1.5.0'], hlapi.CommunityData('ICTSHORE')))
# We get interface name and Cisco interface description for all interfaces
# The last parameter is the OID containing the number of interfaces, so we can loop 'em all!
its = get_bulk_auto('10.0.0.1', [
'1.3.6.1.2.1.2.2.1.2 ',
'1.3.6.1.2.1.31.1.1.1.18'
], hlapi.CommunityData('ICTSHORE'), '1.3.6.1.2.1.2.1.0')
# We print the results in format OID=value
for it in its:
for k, v in it.items():
print("{0}={1}".format(k, v))
# We leave a blank line between the output of each interface
print('')
Hopefully, this will allow you to make some great Python scripts that use SNMP without the pain that used to come with them. What are you going to use this script for? What would you like to see implemented? As always, let me know in the comments!