Search code examples
pythonsnmpnet-snmp

proper getnext logic in a pass_persist handler


I'm working on a small script that'll act as an snmp pass_persist handler. I want it to read a file in (called 'numbers', for now in the same dir) which just contains some integers and return these as an oid tree.

I've been stuck on this for a few days now, and I realise now it's due to a fundamental misunderstanding of how snmpd works. I'm using the snmpd.conf man page which makes no mention of any difference in how 'get' and 'getnext' requests are to be handled, but I assume there is one. I can't for the life of me get snmpwalk to work with this script.

Could someone who knows a little more about snmp look this code over ? I've seen several other versions of pass scripts, including a few in python but I've not been able to see from looking at the code how they handle the protocol differently to my code. I saw one implementation that handled a blank command ( '' ), but others that apparently didn't.

Basically, I'm pretty confused at this point ! - It's also proving pretty hard to debug snmpd as it's the one calling my script, not me. I'm logging what i can, and running snmpd in the foreground, but other than that it's all a bit "black-box".

Can anyone shed some light ?

i.e: numbers file:

101
102
103
I want returned as:
.1.3.6.1.4.1..[snip]..1 = 101
.1.3.6.1.4.1..[snip]..2 = 102
.1.3.6.1.4.1..[snip]..3 = 103

My script (I'm not worried about returning anything other than integers, and i know the file close will never be reached, but it makes me feel better):


#!/bin/python -u

import os,sys, syslog

def getLine():
    return sys.stdin.readline().strip()

def getFileLine(sub_oid, lines):
    sub_oid = int(sub_oid)
    if sub_oid >= len(lines):
        return 'NONE'
    else:
        return lines[sub_oid]

def printOutput(oid, var_type, varbind_value):
    if varbind_value == 'NONE':
        print 'NONE'
    else:
        print oid
        print var_type
        print varbind_value

######################################################

sub_oid = 0
FH = open('numbers','r')
lines = FH.readlines()

while True:
    command = getLine()
    syslog.syslog("command: %s" % command)

    if command == 'PING':
        syslog.syslog('got a ping')
        print 'PONG'
    elif command == 'get':
        given_oid = getLine()
        sub_oid = int(given_oid.split('.')[-1])
        varbind_value = getFileLine(sub_oid, lines)
        printOutput(given_oid, 'integer', varbind_value.strip())
    elif command == 'getnext':
        given_oid = getLine()
        syslog.syslog("got a requested oid of: %s" % given_oid)
        sub_oid = int(given_oid.split('.')[-1])
        varbind_value = getFileLine(sub_oid, lines)
        printOutput(given_oid, 'integer', varbind_value.strip())
    else:
        syslog.syslog("Unknown command: %s" % command)

FH.close()

Solution

  • First of all, there's an already written snmp-passpersist Python module specifically for this task. Its page has a link to a real-world usage example. An example code for your case is below.

    Regarding your specific questions:

    1. The description of getnext is indeed notoriously unclear in both specification and wikipedia. It is explained well in TUT:snmpgetnext - Net-SNMP Wiki.

      In brief, it retrieves the first valid OID (and its value) that goes after the specified one in the agent's hierarchy. The "hierarchy" here can be represented as an ordered list of all OIDs the agent knows of that are valid at the moment.

      • There are two major use cases for this:
        1. The response contains the OID of that "next" value as well as the value itself. So you can walk the hierarchy at the agent by using returned OIDs in subsequent requests. The agent is supposed to return the "not found" error (and pass_persist handler - "NONE") when the hierarchy is exhausted.
        2. You can specify an incomplete OID too, and the agent is supposed to return the first complete one it knows of that matches the one provided.
      • At packet level, getnext is indeed different from get too (its request type ID is 1, get's is 0).
      • You might want to ignore this "magic" altogether and handle it the same way as get: this only means that "walking" and "guessing" will not work (walking might loop indefinitely). This is how a handler I maintained at my last occupation worked and this is just what is happening in your present code too :^).
        • So, the fix is simple: getFileLine(int(sub_oid)+1, lines) - since your code is already smart enough to return "NONE" on exhaustion. "Guessing" will still not work, but... do you need it?
    2. net-snmpd has a plethora of logging options - search "log" on its manpage (and, hell, it's free software! you can always consult or even debug the source when all else fails). But in this particular case, logging stdin and stdout while giving queries with snmpget/snmpgetnext and/or using sniffer is more than enough.

    With the aforementioned snmp-passpersist, your code boils down to:

    base_oid=".1.3.6.1.4.1..[snip]"
    data_file="<path>"
    
    import snmp_passpersist as snmp
    pp=snmp.PassPersist(base_oid)
    
    for l in (l.rstrip() for l in open(data_file)):
        pp.add_int(l,int(l))
    
    pp.start(user_func=lambda:True,refresh=1800)  # If data updates are needed,
                                                  # replace lambda with a real fn
                                                  # and adjust refresh (sec)
    

    If you need to monitor changes to the file, you can either poll it (like the comment above suggests to) or (in Linux) use something like pyinotify - in this case, you'll probably need to replace pp.main_update() before calling pp.start() or otherwise patch up the module's machinery somehow.