Skip to main content

A minimal SNMP implementation

Project description

Overview

I began work on this project in late 2017 or early 2018 in hopes of making a library that is easy and intuitive to use. I worked through multiple preliminary drafts throughout 2018, scrapping each when I discovered shortcomings inherent in the design. The GitHub repository contains a notes.txt file, in which I documented my thoughts as I worked through the several iterations. In short, I find the most popular libraries to be unnecessarily complex or even buggy, (I must say, however, that I actually find Snimpy to be quite elegant, and any slight distaste I have for it is probably a sign that I think too much like a C programmer). I have found myself frustrated in my attempts to contribute to other projects because they were simply too large for me to wrap my head around, and so I decided simply to start from scratch.

This package is written specifically for Python 3. There is no guarantee of correct operation for Python 2.

Version 0.1.x

This version maintains the minimalist spirit of previous versions. Currently only SNMPv1 is supported. There is no support for parsing MIB files, meaning that only numeric OIDs may be used.

Usage is meant to be quite simple. All requests are handled by a "Manager", which is created using the snmp.Manager() factory function. All requests return a list containing entries of type VarBind. VarBind objects have two attributes: name, which is an OID object, and value, which is one of several types representing ASN.1 data types. The snmp.types module defines a base class called ASN1, and several primitives: INTEGER, OCTET_STRING, NULL, OID, and SEQUENCE. Subclasses are defined for each of these according to the 'type' byte used in the ASN.1 encoding (IpAddress, Counter32, etc). These types may be decoded into python primitives with the .value attribute. Note that OCTET_STRINGs will, by default, use bytes, rather than str. Within the rules of ASN.1, OCTET STRINGs may be used to represent non-ASCII data, and attempts to decode such data may unexpectedly cause UnicodeDecodeErrors. The exception to this rule is the IpAddress subtype, which uses socket.inet_ntoa and socket.inet_aton, which turn a bytes object of length 4 into a str, and vice-versa.

Creating a Manager

The Manager object is created using the Manager() factory function. The only argument specific to this function is the version argument. Any other argument provided will be passed directly to the constructor of the Manager for that version. As of version 0.1.x, only version 1 is supported, meaning that the version parameter is not needed.

The SNMPv1 Manager has the following constructor prototype:

__init__(self, community=None, rwcommunity=None, port=161, resend=1)

The community parameter will be used for all requests if the rwcommunity parameter is not provided. On the other hand, you may optionally provide the rwcommunity along with the community argument, in which case rwcommunity will be used for SET requests, and community will be used only for GET and GETNEXT requests.

port is the UDP port number to send requests to.

When a response is not received within a certain window after the request is sent, the manager will re-send the request until a longer timeout period has elapsed. The resend parameter dictates how many seconds to wait for a response before re-sending the original request, and may be a float or an int. The timeout parameter is not given in the constructor, but may be specified when the request is made, allowing different requests to have different timeout values.

Requests

SNMP version 1 supports only 3 types of request: GET, GETNEXT, and SET. A key design improvement introduced in version 0.1.0 of this package is the ability to send requests without blocking to wait for the response. As this feature is inconsistent with most other packages, the default behavior is for the request functions to block until the response is received, and then return the requested values. The details are mostly documented below in the section detailing the GET request, with additional notes made as needed in the other sections.

GET Request

GET requests are performed by calling the get() method on the Manager object. This method has the following prototype:

get(self, host, *oids, community=None, block=True, timeout=10, refresh=False, next=False)

host is the IPv4 Address of the target host. Hostnames or IPv6 addresses are not supported at this time.

oids allows for a variable number of OIDs to be requested at once. The return value of the function will be a list of VarBind objects, where the ith element corresponds to the ith OID. If block is False, a value of None in the returned list indicates that the value for that OID is not yet available.

community allows you to specify an alternate community string to use for this particular request. It is necessary only the first time an OID is requested from a particular host, or when refresh is True. For example, if an initial call is made where block is given as False, the given value of community will be used, but when the blocking call is made later to retrieve the value, community is not needed.

block may be set to False to cause the function to return immediately after the request has been sent, without waiting for the response. The value may be retrieved later by calling the function again, usually with block set to True (or ommitted). This is mostly useful only when a single manager will be used to talk to multiple hosts at once, allowing requests to be processed in parallel, rather than wasting time waiting for each in series.

timeout is the maximum number of seconds to wait for a response. If block is True, a snmp.exceptions.Timeout exception will be raised if a response is not received within that time. If block is False, no exception will be raised until the function is called again to get the response.

refresh will discard any previous values for the requested OIDs that have been stored locally. By default, no network traffic is sent to re-request the value of a variable except for the first request for that variable with a given Manager object, or when refresh is set to True. This should only be set at the time the request is to be sent, meaning that if block and refresh are both True, then when the later call is made to retrieve the value, refresh should be given as False, as the request has already been sent.

next: Setting this parameter to True will send a GETNEXT request, rather than a GET request. The get_next() function also exists, but is simply a wrapper function that calls get() with next set to True.

GETNEXT Request

As mentioned above, the GETNEXT request is actually implemented with the get() function. However, for sake of readability, a get_next() function is also provided. This function accepts all the same parameters as get().

SET Request

SET requests are performed with the set() method, which has the following prototype:

set(self, host, oid, value, community=None, block=True, timeout=10)

While the protocol does technically allow multiple SET operations to be performed in a single request, for simplicity, this function only supports setting a single variable at once, though this may change in the future. The value parameter may be a subtype of ASN1, but if it is given as a python native type, it will be automatically translated.

The community and timeout parameters work exactly as described above for get().

The block parameter introduces some interesting considerations. By default, the function will block until the operation has returned successfully, and will return a list with a single entry for the VarBind contained in the response. On the other hand, if block is given as False, the function will return a value of None once the request has been sent, and the result should then be checked by a call to get() rather than making a second call to set(). Calling set() has the same effect as making a call to get() with refresh set to True, so the get() method is guaranteed not to return until the response for the SET request has been received.

In order to ensure data integrity, the Manager enforces what is essentially a writer preferred reader/writer policy between GET and SET requests. In other words, a SET request cannot be sent as long as there is an outstanding GET request that has not yet received a response. This means that even if block is given as False, the set() method may still block for a short time (up to the timeout for the outstanding GET request) before sending its request.

Walk Operation

As of version 0.1.4, a helper function is available to perform a simple walk. At the current time it only walks on a single variable at a time, but in the future it will be able to walk multiple variables in parallel. The function is implemented as a generator in order to improve responsiveness, but each step of the walk is performed as a blocking GETNEXT operation.

The function prototype is as follows:

walk(self, host, oid, community=None, refresh=False, timeout=10)

Example

import logging
import time

from snmp import Manager
from snmp.exceptions import Timeout

# uncomment this for verbose output
#logging.basicConfig(level=logging.DEBUG)

# REPLACE 'public' with your community string
manager = Manager(b'public')

try:
    hosts = ["10.0.0.2", "10.0.0.3"]                    # REPLACE these IPs with real IPs
    oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.5.0"]   # [SNMPv2-MIB::sysDescr.0, SNMPv2-MIB::sysName.0]

    start = time.time()

    # removing this loop will increase run time on average
    for host in hosts:
        manager.get(host, *oids, block=False, timeout=1)
        manager.get(host, *oids, block=False, timeout=1, next=True)

    for host in hosts:
        vars = manager.get(host, *oids)
        print(host)
        for var in vars:
            print(var)

        vars = manager.get(host, *oids, next=True)
        for var in vars:
            print(var)

    end = time.time()
    print("Took {} seconds".format(end - start))

except Timeout as e:
    print("Request for {} from host {} timed out".format(e, host))

finally:
    manager.close()

IMPORTANT: Manager objects must be closed when then are no longer needed (by calling .close()). Failure to do so may cause a program to hang rather than terminating properly. As of v0.1.5, Managers may be used with context managers, in which case .close() will be called upon exiting the body of the context manager.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snmp-0.1.7.tar.gz (19.5 kB view hashes)

Uploaded Source

Built Distribution

snmp-0.1.7-py3-none-any.whl (28.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page