diff --git a/examples/THREADING-CB-MIB.txt b/examples/THREADING-CB-MIB.txt new file mode 100644 index 0000000..ae78122 --- /dev/null +++ b/examples/THREADING-CB-MIB.txt @@ -0,0 +1,141 @@ +THREADING-CB-MIB DEFINITIONS ::= BEGIN + +------------------------------------------------------------------------ +-- MIB for python-netsnmpagent's example threading_agent.py +-- Copyright (c) 2012-2016 Pieter Hollants +-- Licensed under the GNU Lesser Public License (LGPL) version 3 +------------------------------------------------------------------------ + +-- Imports +IMPORTS + MODULE-IDENTITY, OBJECT-TYPE, NOTIFICATION-TYPE, + Integer32, Unsigned32, Counter32, Counter64, TimeTicks, IpAddress, + enterprises + FROM SNMPv2-SMI + TEXTUAL-CONVENTION, DisplayString + FROM SNMPv2-TC + MODULE-COMPLIANCE, OBJECT-GROUP, NOTIFICATION-GROUP + FROM SNMPv2-CONF + agentxObjects + FROM AGENTX-MIB; + +-- Description and update information +threadingCBMIB MODULE-IDENTITY + LAST-UPDATED "201710230412Z" + ORGANIZATION "N/A" + CONTACT-INFO + "Editor: + Pieter Hollants + EMail: " + DESCRIPTION + "A MIB for python-netsnmpagent's example threading_cb_agent.py" + + REVISION "201710230412Z" + DESCRIPTION + "First version." + + ::= { agentxObjects 101 } + +-- Definition of a generic ThreadingNotificationStatus type +ThreadingNotificationStatus ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "Indicates the enabling or disabling of a particular class of + notifications." + SYNTAX INTEGER { + disabled (0), -- This class of notifications is disabled + enabled (1) -- This class of notifications is enabled +} + +-- Definition of MIB's root nodes + +threadingCBMIBObjects OBJECT IDENTIFIER ::= { threadingCBMIB 1 } +threadingCBMIBNotifications OBJECT IDENTIFIER ::= { threadingCBMIB 2 } +threadingCBMIBConformance OBJECT IDENTIFIER ::= { threadingCBMIB 3 } + +threadingScalars OBJECT IDENTIFIER ::= { threadingCBMIBObjects 1 } + +------------------------------------------------------------------------ +-- Scalars +------------------------------------------------------------------------ + +threadingString OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "A string. Curious about its contents?" + ::= { threadingScalars 1 } + +threadingCBInteger OBJECT-TYPE + SYNTAX Integer32 + MAX-ACCESS read-write + STATUS current + DESCRIPTION + "A read-write, 32-bits integer value." + ::= { threadingScalars 2 } + +threadingCBString OBJECT-TYPE + SYNTAX OctetString + MAX-ACCESS read-write + STATUS current + DESCRIPTION + "A read-write string." + ::= { threadingScalars 3 } + + +------------------------------------------------------------------------ +-- Notifications +------------------------------------------------------------------------ + +events OBJECT IDENTIFIER ::= { threadingCBMIBNotifications 0 } +operation OBJECT IDENTIFIER ::= { threadingCBMIBNotifications 1 } + +threadingStringChange NOTIFICATION-TYPE + OBJECTS { + threadingString + } + STATUS current + DESCRIPTION + "A threadingStringChange notification signifies that there has + been a change to the value of threadingString." + ::= { events 1 } + +threadingStringChangeNotificationsEnabled OBJECT-TYPE + SYNTAX ThreadingNotificationStatus + MAX-ACCESS read-write + STATUS current + DESCRIPTION + "Controls whether threadingStringChange notifications are + enabled or disabled." + ::= { operation 1 } + +------------------------------------------------------------------------ +-- Conformance +------------------------------------------------------------------------ + +threadingMIBGroups OBJECT IDENTIFIER ::= { threadingCBMIBConformance 1 } + +threadingMIBScalarsGroup OBJECT-GROUP + OBJECTS { + threadingString, + threadingCBInteger, + threadingCBString, + threadingStringChangeNotificationsEnabled + } + STATUS current + DESCRIPTION + "A collection of objects related to threadingScalars." + ::= { threadingMIBGroups 1 } + +threadingMIBScalarsNotificationsGroup NOTIFICATION-GROUP + NOTIFICATIONS { + threadingStringChange + } + STATUS current + DESCRIPTION + "The notifications which indicate specific changes in + threadingScalars." + ::= { threadingMIBGroups 2 } + +END diff --git a/examples/run_threading_cb_agent.sh b/examples/run_threading_cb_agent.sh new file mode 100644 index 0000000..a205175 --- /dev/null +++ b/examples/run_threading_cb_agent.sh @@ -0,0 +1,100 @@ +# +# python-netsnmpagent example agent with threading and callback +# +# Copyright (c) 2013-2016 Pieter Hollants +# Licensed under the GNU Lesser Public License (LGPL) version 3 +# + +# +# This script makes running threading_agent.py easier for you because it takes +# care of setting everything up so that the example agent can be run +# successfully. +# + +set -u +set -e + +# Find path to snmpd executable +SNMPD_BIN="" +for DIR in /usr/local/sbin /usr/sbin +do + if [ -x $DIR/snmpd ] ; then + SNMPD_BIN=$DIR/snmpd + break + fi +done +if [ -z "$SNMPD_BIN" ] ; then + echo "snmpd executable not found -- net-snmp not installed?" + exit 1 +fi + +# Make sure we leave a clean system upon exit +cleanup() { + if [ -n "$TMPDIR" -a -d "$TMPDIR" ] ; then + # Terminate snmpd, if running + if [ -n "$SNMPD_PIDFILE" -a -e "$SNMPD_PIDFILE" ] ; then + PID="$(cat $SNMPD_PIDFILE)" + if [ -n "$PID" ] ; then + kill -TERM "$PID" + fi + fi + + echo "* Cleaning up..." + + # Clean up temporary directory + rm -rf "$TMPDIR" + fi + + # Make sure echo is back on + stty echo +} +trap cleanup EXIT QUIT TERM INT HUP + +echo "* Preparing snmpd environment..." + +# Create a temporary directory +TMPDIR="$(mktemp --directory --tmpdir threading_agent.XXXXXXXXXX)" +SNMPD_CONFFILE=$TMPDIR/snmpd.conf +SNMPD_PIDFILE=$TMPDIR/snmpd.pid + +# Create a minimal snmpd configuration for our purposes +cat <>$SNMPD_CONFFILE +[snmpd] +rocommunity public 127.0.0.1 +rwcommunity simple 127.0.0.1 +agentaddress localhost:5555 +informsink localhost:5556 +smuxsocket localhost:5557 +master agentx +agentXSocket $TMPDIR/snmpd-agentx.sock + +[snmp] +persistentDir $TMPDIR/state +EOF +touch $TMPDIR/mib_indexes + +# Start a snmpd instance for testing purposes, run as the current user and +# and independent from any other running snmpd instance +$SNMPD_BIN -r -LE warning -C -c$SNMPD_CONFFILE -p$SNMPD_PIDFILE + +# Give the user guidance +echo "* Our snmpd instance is now listening on localhost, port 5555." +echo " From a second console, use the net-snmp command line utilities like this:" +echo "" +echo " cd `pwd`" +echo " snmpwalk -v 2c -c public -M+. localhost:5555 THREADING-CB-MIB::threadingMIB" +echo " snmpget -v 2c -c public -M+. localhost:5555 THREADING-CB-MIB::threadingString.0" +echo " To test the callbacks, read and write them:" +echo " snmpget -v 2c -c public -M+. localhost:5555 THREADING-CB-MIB::threadingCBString.0" +echo " snmpset -v 2c -c simple -M+. localhost:5555 THREADING-CB-MIB::threadingCBString.0 s \"test string\"" +echo " snmpget -v 2c -c public -M+. localhost:5555 THREADING-CB-MIB::threadingCBInteger.0" +echo " snmpset -v 2c -c simple -M+. localhost:5555 THREADING-CB-MIB::threadingCBInteger.0 i 123456" +echo "" + +# Workaround to have CTRL-C not generate any visual feedback (we don't do any +# input anyway) +stty -echo + +# Now start the threading agent +echo "* Starting the threading agent..." +python threading_cb_agent.py -m $TMPDIR/snmpd-agentx.sock -p $TMPDIR/ diff --git a/examples/threading_cb_agent.py b/examples/threading_cb_agent.py new file mode 100644 index 0000000..7ed20a0 --- /dev/null +++ b/examples/threading_cb_agent.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# +# python-netsnmpagent example agent with threading and callback support +# +# Copyright (c) 2013-2016 Pieter Hollants +# Licensed under the GNU Lesser Public License (LGPL) version 3 +# + +# +# simple_agent.py demonstrates registering the various SNMP object types quite +# nicely but uses an inferior control flow logic: the main loop blocks in +# net-snmp's check_and_process() call until some event happens (eg. SNMP +# requests need processing). Only then will data be updated, not inbetween. And +# on the other hand, SNMP requests can not be handled while data is being +# updated, which might take longer periods of time. +# +# This example agent uses a more real life-suitable approach by outsourcing the +# data update process into a separate thread that gets woken up through an +# SIGALRM handler at an configurable interval. This does only ensure periodic +# data updates, it also makes sure that SNMP requests will always be replied to +# in time. +# +# Note that this implementation does not address possible locking issues: if +# a SNMP client's requests are processed while the data update thread is in the +# midst of refreshing the SNMP objects, the client might receive partially +# inconsistent data. +# +# Use the included script run_threading_agent.sh to test this example. +# +# Alternatively, see the comment block in the head of simple_agent.py for +# adaptable instructions how to run this example against a system-wide snmpd +# instance. +# + +import sys, os, signal, time +import optparse, threading, subprocess + +# Make sure we use the local copy, not a system-wide one +sys.path.insert(0, os.path.dirname(os.getcwd())) +import netsnmpagent +from netsnmpapi import * + +prgname = sys.argv[0] + +# Process command line arguments +parser = optparse.OptionParser() +parser.add_option( + "-i", + "--interval", + dest="interval", + help="Set interval in seconds between data updates", + default=30 +) +parser.add_option( + "-m", + "--mastersocket", + dest="mastersocket", + help="Sets the transport specification for the master agent's AgentX socket", + default="/var/run/agentx/master" +) +parser.add_option( + "-p", + "--persistencedir", + dest="persistencedir", + help="Sets the path to the persistence directory", + default="/var/lib/net-snmp" +) +(options, args) = parser.parse_args() + +headerlogged = 0 +def LogMsg(msg): + """ Writes a formatted log message with a timestamp to stdout. """ + + global headerlogged + + if headerlogged == 0: + print("{0:<8} {1:<90} {2}".format( + "Time", + "MainThread", + "UpdateSNMPObjsThread" + )) + print("{0:-^120}".format("-")) + headerlogged = 1 + + threadname = threading.currentThread().name + + funcname = sys._getframe(1).f_code.co_name + if funcname == "": + funcname = "Main code path" + elif funcname == "LogNetSnmpMsg": + funcname = "net-snmp code" + else: + funcname = "{0}()".format(funcname) + + if threadname == "MainThread": + logmsg = "{0} {1:<112.112}".format( + time.strftime("%T", time.localtime(time.time())), + "{0}: {1}".format(funcname, msg) + ) + else: + logmsg = "{0} {1:>112.112}".format( + time.strftime("%T", time.localtime(time.time())), + "{0}: {1}".format(funcname, msg) + ) + print(logmsg) + +def LogNetSnmpMsg(priority, msg): + """ Log handler for log messages generated by net-snmp code. """ + + LogMsg("[{0}] {1}.".format(priority, msg)) + +# callback functions for special handling of GET or SET requests +def threadingCBInteger_callback(mib_handler, handler_reg, agent_req, request_info): + req_mode = agent_req[0].mode + + # if you need the Python agent variable, it can be found via the MIB object name + #pyvar = agent._objs[""]['THREADING-CB-MIB::threadingCBInteger'] + + if req_mode == MODE_GET or req_mode == MODE_GET_NEXT: + print("GET callback for threadingCBUnsigned") + # it's already visible to the watcher handler, so no need to + # do anything special, unless you need or want to + pass + elif req_mode == MODE_SET_RESERVE1: + # verify type matches expectations + if request_info[0].requestvb[0].type != ASN_INTEGER: + libnsa.netsnmp_request_set_error(request_info, SNMP_ERR_WRONGTYPE) + elif req_mode == MODE_SET_ACTION: + # we can simplify things by not supporting undo and rollback + newval = request_info[0].requestvb[0].val.integer[0] + print("SET_ACTION callback for threadingCBUnsigned, new val = ", newval) + + return SNMP_ERR_NOERROR + +def threadingCBString_callback(mib_handler, handler_reg, agent_req, request_info): + req_mode = agent_req[0].mode + + # if you need the Python agent variable, it can be found via the MIB object name + #pyvar = agent._objs[""]['THREADING-CB-MIB::threadingCBString'] + + if req_mode == MODE_GET or req_mode == MODE_GET_NEXT: + print("GET callback for threadingCBString") + # it's already visible to the watcher handler, so no need to + # do anything special, unless you need or want to + pass + elif req_mode == MODE_SET_RESERVE1: + # verify type matches expectations + if request_info[0].requestvb[0].type != ASN_OCTET_STR: + libnsa.netsnmp_request_set_error(request_info, SNMP_ERR_WRONGTYPE) + elif req_mode == MODE_SET_ACTION: + # we can simplify things by not supporting undo and rollback + newval = request_info[0].requestvb[0].val.string + print("SET_ACTION callback for threadingCBString, new val = ", newval) + + return SNMP_ERR_NOERROR + +# Create an instance of the netsnmpAgent class +try: + agent = netsnmpagent.netsnmpAgent( + AgentName = "ThreadingAgent", + MasterSocket = options.mastersocket, + PersistenceDir = options.persistencedir, + MIBFiles = [ os.path.abspath(os.path.dirname(sys.argv[0])) + + "/THREADING-CB-MIB.txt" ], + LogHandler = LogNetSnmpMsg, + ) +except netsnmpagent.netsnmpAgentException as e: + print("{0}: {1}".format(prgname, e)) + sys.exit(1) + +# Register the SNMP objects we serve, a DisplayString, an Unsigned and OctetString +threadingString = agent.DisplayString( + oidstr = "THREADING-CB-MIB::threadingString", + initval = "" +) +threadingCBInteger = agent.Integer32( + oidstr = "THREADING-CB-MIB::threadingCBInteger", + initval = 0, + callback = threadingCBInteger_callback +) +threadingCBString = agent.OctetString( + oidstr = "THREADING-CB-MIB::threadingCBString", + initval = "", + callback = threadingCBString_callback +) + +def UpdateSNMPObjs(): + """ Function that does the actual data update. """ + + global threadingString + + LogMsg("Beginning data update.") + data = "" + + # Obtain the data by calling an external command. We don't use + # subprocess.check_output() here for compatibility with Python versions + # older than 2.7. + LogMsg("Calling external command \"sleep 10; date\".") + proc = subprocess.Popen( + "sleep 10; date", shell=True, env={ "LANG": "C" }, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + output = proc.communicate()[0].splitlines()[0] + rc = proc.poll() + if rc != 0: + LogMsg("An error occured executing the command: {0}".format(output)) + return + + msg = "Updating \"threadingString\" object with data \"{0}\"." + LogMsg(msg.format(output)) + threadingString.update(output) + + LogMsg("Data update done, exiting thread.") + +def UpdateSNMPObjsAsync(): + """ Starts UpdateSNMPObjs() in a separate thread. """ + + # UpdateSNMPObjs() will be executed in a separate thread so that the main + # thread can continue looping and processing SNMP requests while the data + # update is still in progress. However we'll make sure only one update + # thread is run at any time, even if the data update interval has been set + # too low. + if threading.active_count() == 1: + LogMsg("Creating thread for UpdateSNMPObjs().") + t = threading.Thread(target=UpdateSNMPObjs, name="UpdateSNMPObjsThread") + t.daemon = True + t.start() + else: + LogMsg("Data update still active, data update interval too low?") + +# Start the agent (eg. connect to the master agent). +try: + agent.start() +except netsnmpagent.netsnmpAgentException as e: + LogMsg("{0}: {1}".format(prgname, e)) + sys.exit(1) + +# Trigger initial data update. +LogMsg("Doing initial call to UpdateSNMPObjsAsync().") +UpdateSNMPObjsAsync() + +# Install a signal handler that terminates our threading agent when CTRL-C is +# pressed or a KILL signal is received +def TermHandler(signum, frame): + global loop + loop = False +signal.signal(signal.SIGINT, TermHandler) +signal.signal(signal.SIGTERM, TermHandler) + +# Define a signal handler that takes care of updating the data periodically +def AlarmHandler(signum, frame): + global loop, timer_triggered + + LogMsg("Got triggered by SIGALRM.") + + if loop: + timer_triggered = True + + UpdateSNMPObjsAsync() + + signal.signal(signal.SIGALRM, AlarmHandler) + signal.setitimer(signal.ITIMER_REAL, float(options.interval)) +msg = "Installing SIGALRM handler triggered every {0} seconds." +msg = msg.format(options.interval) +LogMsg(msg) +signal.signal(signal.SIGALRM, AlarmHandler) +signal.setitimer(signal.ITIMER_REAL, float(options.interval)) + +# The threading agent's main loop. We loop endlessly until our signal +# handler above changes the "loop" variable. +LogMsg("Now serving SNMP requests, press ^C to terminate.") + +loop = True +while loop: + # Block until something happened (signal arrived, SNMP packets processed) + timer_triggered = False + res = agent.check_and_process() + if res == -1 and not timer_triggered and loop: + loop = False + LogMsg("Error {0} in SNMP packet processing!".format(res)) + elif loop and timer_triggered: + LogMsg("net-snmp's check_and_process() returned due to SIGALRM (res={0}), doing another loop.".format(res)) + elif loop: + LogMsg("net-snmp's check_and_process() returned (res={0}), doing another loop.".format(res)) + +LogMsg("Terminating.") +agent.shutdown() diff --git a/netsnmpagent.py b/netsnmpagent.py index f63be6e..1068bbe 100644 --- a/netsnmpagent.py +++ b/netsnmpagent.py @@ -338,7 +338,7 @@ def _py_index_stop_callback(majorID, minorID, serverarg, clientarg): # Initialize our SNMP object registry self._objs = defaultdict(dict) - def _prepareRegistration(self, oidstr, writable = True): + def _prepareRegistration(self, oidstr, writable = True, callback_handler = None): """ Prepares the registration of an SNMP object. "oidstr" is the OID to register the object at. @@ -383,7 +383,7 @@ def _prepareRegistration(self, oidstr, writable = True): # left to net-snmp. handler_reginfo = libnsa.netsnmp_create_handler_registration( b(oidstr), - None, + callback_handler, oid, oid_len, handler_modes @@ -411,12 +411,14 @@ def VarTypeClass(property_func): from an ASN.1 perspective, eg. ASN_INTEGER. - "context" : A string defining the context name for the SNMP variable + - "callback" : An optional function that is called when a + SNMP variable is retrieved or set The class instance returned will have no association with net-snmp yet. Use the Register() method to associate it with an OID. """ # This is the replacement function, the "decoration" - def create_vartype_class(self, initval = None, oidstr = None, writable = True, context = ""): + def create_vartype_class(self, initval = None, oidstr = None, writable = True, context = "", callback = None): agent = self # Call the original property_func to retrieve this variable type's @@ -457,7 +459,15 @@ def __init__(self): if oidstr: # Prepare the netsnmp_handler_registration structure. - handler_reginfo = agent._prepareRegistration(oidstr, writable) + self._callback_handler = None + if callback != None: + # We defined a Python function that needs a ctypes conversion so it can + # be called by C code such as net-snmp. That's what SNMPCallback2() is + # used for. However we also need to store the reference in "self" as it + # will otherwise be lost at the exit of this function so that net-snmp's + # attempt to call it would end in nirvana... + self._callback_handler = SNMPCallback2(callback) + handler_reginfo = agent._prepareRegistration(oidstr, writable, self._callback_handler) handler_reginfo.contents.contextName = b(context) # Create the netsnmp_watcher_info structure. @@ -489,13 +499,14 @@ def __init__(self): def value(self): val = self._cvar.value - if isnum(val): - # Python 2.x will automatically switch from the "int" - # type to the "long" type, if necessary. Python 3.x - # has no limits on the "int" type anymore. - val = int(val) - else: - val = u(val) + if self._asntype != ASN_OPAQUE_FLOAT and self._asntype != ASN_OPAQUE_DOUBLE: + if isnum(val): + # Python 2.x will automatically switch from the "int" + # type to the "long" type, if necessary. Python 3.x + # has no limits on the "int" type anymore. + val = int(val) + else: + val = u(val) return val @@ -506,8 +517,10 @@ def cref(self, **kwargs): def update(self, val): if self._asntype == ASN_COUNTER and val >> 32: val = val & 0xFFFFFFFF - if self._asntype == ASN_COUNTER64 and val >> 64: + elif self._asntype == ASN_COUNTER64 and val >> 64: val = val & 0xFFFFFFFFFFFFFFFF + elif self.__class__.__name__ == 'Gauge32' and val >> 32: + val = 0xFFFFFFFF self._cvar.value = val if props["flags"] == WATCHER_MAX_SIZE: if len(val) > self._max_size: @@ -529,7 +542,7 @@ def increment(self, count=1): return create_vartype_class @VarTypeClass - def Integer32(self, initval = None, oidstr = None, writable = True, context = ""): + def Integer32(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.c_long, "flags" : WATCHER_FIXED_SIZE, @@ -538,7 +551,7 @@ def Integer32(self, initval = None, oidstr = None, writable = True, context = "" } @VarTypeClass - def Unsigned32(self, initval = None, oidstr = None, writable = True, context = ""): + def Unsigned32(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.c_ulong, "flags" : WATCHER_FIXED_SIZE, @@ -547,7 +560,16 @@ def Unsigned32(self, initval = None, oidstr = None, writable = True, context = " } @VarTypeClass - def Counter32(self, initval = None, oidstr = None, writable = True, context = ""): + def Gauge32(self, initval = None, oidstr = None, writable = True, context = "", callback = None): + return { + "ctype" : ctypes.c_ulong, + "flags" : WATCHER_FIXED_SIZE, + "initval" : 0, + "asntype" : ASN_GAUGE + } + + @VarTypeClass + def Counter32(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.c_ulong, "flags" : WATCHER_FIXED_SIZE, @@ -556,7 +578,7 @@ def Counter32(self, initval = None, oidstr = None, writable = True, context = "" } @VarTypeClass - def Counter64(self, initval = None, oidstr = None, writable = True, context = ""): + def Counter64(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : counter64, "flags" : WATCHER_FIXED_SIZE, @@ -565,7 +587,7 @@ def Counter64(self, initval = None, oidstr = None, writable = True, context = "" } @VarTypeClass - def TimeTicks(self, initval = None, oidstr = None, writable = True, context = ""): + def TimeTicks(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.c_ulong, "flags" : WATCHER_FIXED_SIZE, @@ -573,13 +595,31 @@ def TimeTicks(self, initval = None, oidstr = None, writable = True, context = "" "asntype" : ASN_TIMETICKS } + @VarTypeClass + def Float(self, initval = None, oidstr = None, writable = True, context = "", callback = None): + return { + "ctype" : ctypes.c_float, + "flags" : WATCHER_FIXED_SIZE, + "initval" : 0, + "asntype" : ASN_OPAQUE_FLOAT + } + + @VarTypeClass + def Double(self, initval = None, oidstr = None, writable = True, context = "", callback = None): + return { + "ctype" : ctypes.c_double, + "flags" : WATCHER_FIXED_SIZE, + "initval" : 0, + "asntype" : ASN_OPAQUE_DOUBLE + } + # Note we can't use ctypes.c_char_p here since that creates an immutable # type and net-snmp _can_ modify the buffer (unless writable is False). # Also note that while net-snmp 5.5 introduced a WATCHER_SIZE_STRLEN flag, # we have to stick to WATCHER_MAX_SIZE for now to support net-snmp 5.4.x # (used eg. in SLES 11 SP2 and Ubuntu 12.04 LTS). @VarTypeClass - def OctetString(self, initval = None, oidstr = None, writable = True, context = ""): + def OctetString(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.create_string_buffer, "flags" : WATCHER_MAX_SIZE, @@ -591,7 +631,7 @@ def OctetString(self, initval = None, oidstr = None, writable = True, context = # Whereas an OctetString can contain UTF-8 encoded characters, a # DisplayString is restricted to ASCII characters only. @VarTypeClass - def DisplayString(self, initval = None, oidstr = None, writable = True, context = ""): + def DisplayString(self, initval = None, oidstr = None, writable = True, context = "", callback = None): return { "ctype" : ctypes.create_string_buffer, "flags" : WATCHER_MAX_SIZE, @@ -602,7 +642,7 @@ def DisplayString(self, initval = None, oidstr = None, writable = True, context # IP addresses are stored as unsigned integers, but the Python interface # should use strings. So we need a special class. - def IpAddress(self, initval = "0.0.0.0", oidstr = None, writable = True, context = ""): + def IpAddress(self, initval = "0.0.0.0", oidstr = None, writable = True, context = "", callback = None): agent = self class IpAddress(object): @@ -616,7 +656,15 @@ def __init__(self): if oidstr: # Prepare the netsnmp_handler_registration structure. - handler_reginfo = agent._prepareRegistration(oidstr, writable) + self._callback_handler = None + if callback != None: + # We defined a Python function that needs a ctypes conversion so it can + # be called by C code such as net-snmp. That's what SNMPCallback2() is + # used for. However we also need to store the reference in "self" as it + # will otherwise be lost at the exit of this function so that net-snmp's + # attempt to call it would end in nirvana... + self._callback_handler = SNMPCallback2(callback) + handler_reginfo = agent._prepareRegistration(oidstr, writable, self._callback_handler) handler_reginfo.contents.contextName = b(context) # Create the netsnmp_watcher_info structure. @@ -669,7 +717,72 @@ def update(self, val): # Return an instance of the just-defined class to the agent return IpAddress() - def Table(self, oidstr, indexes, columns, counterobj = None, extendable = False, context = ""): + # TruthValues are stored as integers, but the Python interface + # should use bool, so we need a special class. + def TruthValue(self, initval = False, oidstr = None, writable = True, context = "", callback = None): + agent = self + + class TruthValue(object): + def __init__(self): + self._flags = WATCHER_FIXED_SIZE + self._asntype = ASN_INTEGER + self._cvar = ctypes.c_int(0) + self._data_size = ctypes.sizeof(self._cvar) + self._max_size = self._data_size + self.update(initval) + + if oidstr: + # Prepare the netsnmp_handler_registration structure. + self._callback_handler = None + if callback != None: + # We defined a Python function that needs a ctypes conversion so it can + # be called by C code such as net-snmp. That's what SNMPCallback2() is + # used for. However we also need to store the reference in "self" as it + # will otherwise be lost at the exit of this function so that net-snmp's + # attempt to call it would end in nirvana... + self._callback_handler = SNMPCallback2(callback) + handler_reginfo = agent._prepareRegistration(oidstr, writable, self._callback_handler) + handler_reginfo.contents.contextName = b(context) + + # Create the netsnmp_watcher_info structure. + watcher = libnsX.netsnmp_create_watcher_info( + self.cref(), + ctypes.sizeof(self._cvar), + ASN_INTEGER, + WATCHER_FIXED_SIZE + ) + watcher._maxsize = ctypes.sizeof(self._cvar) + + # Register handler and watcher with net-snmp. + result = libnsX.netsnmp_register_watched_instance( + handler_reginfo, + watcher + ) + if result != 0: + raise netsnmpAgentException("Error registering variable with net-snmp!") + + # Finally, we keep track of all registered SNMP objects for the + # getRegistered() method. + agent._objs[context][oidstr] = self + + def value(self): + # Get boolean representation of TruthValue. + return True if self._cvar.value == TV_TRUE else False + + def cref(self, **kwargs): + return ctypes.byref(self._cvar) + + def update(self, val): + # Convert boolean to corresponding integer values + if isinstance(val, bool): + self._cvar.value = TV_TRUE if val == True else TV_FALSE + else: + raise netsnmpAgentException("TruthValue must be True or False") + + # Return an instance of the just-defined class to the agent + return TruthValue() + + def Table(self, oidstr, indexes, columns, counterobj = None, extendable = False, context = "", callback = None): agent = self # Define a Python class to provide access to the table. @@ -713,7 +826,8 @@ def __init__(self, oidstr, idxobjs, coldefs, counterobj, extendable, context): # Register handler and table_data_set with net-snmp. self._handler_reginfo = agent._prepareRegistration( oidstr, - extendable + extendable, + callback ) self._handler_reginfo.contents.contextName = b(context) result = libnsX.netsnmp_register_table_data_set( diff --git a/netsnmpapi.py b/netsnmpapi.py index 99e6b8e..7c59841 100644 --- a/netsnmpapi.py +++ b/netsnmpapi.py @@ -81,6 +81,27 @@ class snmp_log_message(ctypes.Structure): pass ("msg", ctypes.c_char_p) ] +# counter64 requires some extra work because it can't be reliably represented +# by a single C data type +class counter64(ctypes.Structure): + @property + def value(self): + return self.high << 32 | self.low + + @value.setter + def value(self, val): + self.high = val >> 32 + self.low = val & 0xFFFFFFFF + + def __init__(self, initval=0): + ctypes.Structure.__init__(self, 0, 0) + self.value = initval +counter64_p = ctypes.POINTER(counter64) +counter64._fields_ = [ + ("high", ctypes.c_ulong), + ("low", ctypes.c_ulong) +] + # include/net-snmp/library/snmp_api.h SNMPERR_SUCCESS = 0 @@ -111,6 +132,16 @@ class snmp_log_message(ctypes.Structure): pass # include/net-snmp/library/snmp.h SNMP_ERR_NOERROR = 0 +SNMP_ERR_TOO_BIG = 1 +SNMP_ERR_NOSUCHNAME = 2 +SNMP_ERR_BADVALUE = 3 +SNMP_ERR_READONLY = 4 +SNMP_ERR_GENERR = 5 +SNMP_ERR_NOACCESS = 6 +SNMP_ERR_WRONGTYPE = 7 +SNMP_ERR_RESOURCEUNAVAILABLE = 13 +SNMP_ERR_COMMITFAILED = 14 +SNMP_ERR_UNDOFAILED = 15 for f in [ libnsa.init_snmp ]: f.argtypes = [ @@ -191,9 +222,76 @@ class netsnmp_handler_registration(ctypes.Structure): pass ("my_reg_void", ctypes.c_void_p) ] +class netsnmp_agent_request_info(ctypes.Structure): pass +netsnmp_agent_request_info_p = ctypes.POINTER(netsnmp_agent_request_info) +netsnmp_agent_request_info._fields_ = [ + ("mode", ctypes.c_int), + ("asp", ctypes.c_void_p), + ("agent_data", ctypes.c_void_p) +] + +# include/net-snmp/types.h +class netsnmp_vardata(ctypes.Union): pass +netsnmp_vardata._fields_ = [ + ("integer", ctypes.POINTER(ctypes.c_long)), + ("string", ctypes.c_char_p), + ("objid", c_oid_p), + ("bitstring", ctypes.POINTER(ctypes.c_ubyte)), + ("counter64", ctypes.POINTER(counter64)), + ("floatVal", ctypes.POINTER(ctypes.c_float)), + ("doubleVal", ctypes.POINTER(ctypes.c_double)) +] + +class netsnmp_variable_list(ctypes.Structure): pass +netsnmp_variable_list_p = ctypes.POINTER(netsnmp_variable_list) +netsnmp_variable_list_p_p = ctypes.POINTER(netsnmp_variable_list_p) +netsnmp_variable_list._fields_ = [ + ("next_variable", netsnmp_variable_list_p), + ("name", c_oid_p), + ("name_length", ctypes.c_size_t), + ("type", ctypes.c_ubyte), + ("val", netsnmp_vardata), + ("val_len", ctypes.c_size_t), + ("name_loc", c_oid * MAX_OID_LEN), + ("buf", ctypes.c_byte * 40), + ("data", ctypes.c_void_p), + ("dataFreeHook", ctypes.c_void_p), + ("index", ctypes.c_int) +] + +class netsnmp_request_info(ctypes.Structure): pass +netsnmp_request_info_p = ctypes.POINTER(netsnmp_request_info) +netsnmp_request_info._fields_ = [ + ("requestvb", netsnmp_variable_list_p), + ("parent_data", ctypes.c_void_p), + ("agent_req_info", ctypes.c_void_p), + ("range_end", c_oid_p), + ("range_end_len", ctypes.c_size_t), + ("delegated", ctypes.c_int), + ("processed", ctypes.c_int), + ("inclusive", ctypes.c_int), + ("status", ctypes.c_int), + ("index", ctypes.c_int), + ("repeat", ctypes.c_int), + ("orig_repeat", ctypes.c_int), + ("requestvb_start", ctypes.c_void_p), + ("next", ctypes.c_void_p), + ("prev", ctypes.c_void_p), + ("subtree", ctypes.c_void_p) +] + +SNMPCallback2 = ctypes.CFUNCTYPE( + ctypes.c_int, # return type + netsnmp_mib_handler_p, # netsnmp_mib_handler *handler + netsnmp_handler_registration_p, # netsnmp_handler_registration *reginfo + netsnmp_agent_request_info_p, # netsnmp_agent_request_info *reginfo + netsnmp_request_info_p # netsnmp_request_info *requests +) + for f in [ libnsa.netsnmp_create_handler_registration ]: f.argtypes = [ ctypes.c_char_p, # const char *name + #SNMPCallback2, # Netsnmp_Node_Handler *handler_access_method ctypes.c_void_p, # Netsnmp_Node_Handler *handler_access_method c_oid_p, # const oid *reg_oid ctypes.c_size_t, # size_t reg_oid_len @@ -201,38 +299,32 @@ class netsnmp_handler_registration(ctypes.Structure): pass ] f.restype = netsnmp_handler_registration_p +for f in [ libnsa.netsnmp_request_set_error ]: + f.argtypes = [ + netsnmp_request_info_p, # netsnmp_request_info *request + ctypes.c_int # int error number + ] + f.restype = ctypes.c_int + # include/net-snmp/library/asn1.h ASN_INTEGER = 0x02 ASN_OCTET_STR = 0x04 +ASN_OPAQUE_TAG2 = 0x30 ASN_APPLICATION = 0x40 - -# counter64 requires some extra work because it can't be reliably represented -# by a single C data type -class counter64(ctypes.Structure): - @property - def value(self): - return self.high << 32 | self.low - - @value.setter - def value(self, val): - self.high = val >> 32 - self.low = val & 0xFFFFFFFF - - def __init__(self, initval=0): - ctypes.Structure.__init__(self, 0, 0) - self.value = initval -counter64_p = ctypes.POINTER(counter64) -counter64._fields_ = [ - ("high", ctypes.c_ulong), - ("low", ctypes.c_ulong) -] +# opaque special types +ASN_OPAQUE_FLOAT = ASN_OPAQUE_TAG2 + (ASN_APPLICATION | 8) +ASN_OPAQUE_DOUBLE = ASN_OPAQUE_TAG2 + (ASN_APPLICATION | 9) # include/net-snmp/library/snmp_impl.h ASN_IPADDRESS = ASN_APPLICATION | 0 ASN_COUNTER = ASN_APPLICATION | 1 ASN_UNSIGNED = ASN_APPLICATION | 2 +ASN_GAUGE = ASN_APPLICATION | 2 ASN_TIMETICKS = ASN_APPLICATION | 3 ASN_COUNTER64 = ASN_APPLICATION | 6 +# opaque special types +ASN_FLOAT = ASN_APPLICATION | 8 +ASN_DOUBLE = ASN_APPLICATION | 9 # include/net-snmp/agent/watcher.h WATCHER_FIXED_SIZE = 0x01 @@ -273,11 +365,6 @@ class netsnmp_watcher_info(ctypes.Structure): pass ] f.restype = ctypes.c_int -# include/net-snmp/types.h -class netsnmp_variable_list(ctypes.Structure): pass -netsnmp_variable_list_p = ctypes.POINTER(netsnmp_variable_list) -netsnmp_variable_list_p_p = ctypes.POINTER(netsnmp_variable_list_p) - # include/net-snmp/varbind_api.h for f in [ libnsa.snmp_varlist_add_variable ]: f.argtypes = [ @@ -415,3 +502,17 @@ class netsnmp_table_data_set(ctypes.Structure): pass ctypes.c_int # int block ] f.restype = ctypes.c_int + +MODE_GET = 160 # SNMP_MSG_GET +MODE_GET_NEXT = 161 # SNMP_MSG_GET_NEXT +MODE_SET_BEGIN = -1 # SNMP_MSG_INTERNAL_SET_BEGIN +MODE_SET_RESERVE1 = 0 # SNMP_MSG_INTERNAL_SET_RESERVE1 +MODE_SET_RESERVE2 = 1 # SNMP_MSG_INTERNAL_SET_RESERVE2 +MODE_SET_ACTION = 2 # SNMP_MSG_INTERNAL_SET_ACTION +MODE_SET_COMMIT = 3 # SNMP_MSG_INTERNAL_SET_COMMIT +MODE_SET_FREE = 4 # SNMP_MSG_INTERNAL_SET_FREE +MODE_SET_UNDO = 5 # SNMP_MSG_INTERNAL_SET_UNDO + +# include/net-snmp/agent/snmp-tc.h +TV_TRUE = 1 +TV_FALSE = 2