From 2bc05c4d18144d3bfbf7f23294a4f3e0624742b9 Mon Sep 17 00:00:00 2001 From: Anton Todorov Date: Wed, 15 Apr 2015 15:36:46 +0300 Subject: [PATCH 1/2] move OID parsing to _prepareOID function extract from _prepareRegistration() to separate function _prepareOID() which returns tuple of (c_oid) oid and (size_t) oid_len The code is improved by trimming trailing zeroes from the oid array --- netsnmpagent.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/netsnmpagent.py b/netsnmpagent.py index 66da16b..dc0f8dc 100644 --- a/netsnmpagent.py +++ b/netsnmpagent.py @@ -313,18 +313,11 @@ def _py_index_stop_callback(majorID, minorID, serverarg, clientarg): # Initialize our SNMP object registry self._objs = defaultdict(dict) - def _prepareRegistration(self, oidstr, writable = True): - """ Prepares the registration of an SNMP object. - - "oidstr" is the OID to register the object at. - "writable" indicates whether "snmpset" is allowed. """ - - # Make sure the agent has not been start()ed yet - if self._status != netsnmpAgentStatus.REGISTRATION: - raise netsnmpAgentException("Attempt to register SNMP object " \ - "after agent has been started!") - - + def _prepareOID(self, oidstr): + """ Convert OID to c_oid type. + "oidstr" is the oid to read + Return tuple c_oid array and c_size_t length + """ if self.UseMIBFiles: # We can't know the length of the internal OID representation # beforehand, so we use a MAX_OID_LEN sized buffer for the call to @@ -339,6 +332,10 @@ def _prepareRegistration(self, oidstr, writable = True): ctypes.byref(oid_len) ) == 0: raise netsnmpAgentException("read_objid({0}) failed!".format(oidstr)) + + # trim trailing zeroes in oid array + trimOID = c_oid * oid_len.value + oid = trimOID( *oid[0:oid_len.value] ) else: # Interpret the given oidstr as the oid itself. try: @@ -348,7 +345,22 @@ def _prepareRegistration(self, oidstr, writable = True): oid = (c_oid * len(parts))(*parts) oid_len = ctypes.c_size_t(len(parts)) + + return (oid, oid_len) + + + def _prepareRegistration(self, oidstr, writable = True): + """ Prepares the registration of an SNMP object. + + "oidstr" is the OID to register the object at. + "writable" indicates whether "snmpset" is allowed. """ + + # Make sure the agent has not been start()ed yet + if self._status != netsnmpAgentStatus.REGISTRATION: + raise netsnmpAgentException("Attempt to register SNMP object " \ + "after agent has been started!") + (oid, oid_len) = self._prepareOID( oidstr ) # Do we allow SNMP SETting to this OID? handler_modes = HANDLER_CAN_RWRITE if writable \ From 82426369445d8ea88029cc6203238d1a1bdb205e Mon Sep 17 00:00:00 2001 From: Anton Todorov Date: Wed, 15 Apr 2015 15:37:43 +0300 Subject: [PATCH 2/2] add send_trap function depending on function arguments it is possible to send all types of traps actually tested v1 and v2c traps, informs are received too When sending traps with Counter64 there is warning issued by net-snmp: send_trap: v1 traps can't carry Counter64 varbinds send_trap: failed to convert v2->v1 template PDU In this case snmptrapd receives the trap only on trap2sink and informsink interfaces --- examples/run_simple_agent_trap.sh | 166 +++++++++++++++++ examples/run_threading_agent_trap.sh | 167 +++++++++++++++++ examples/simple_agent_trap.py | 264 +++++++++++++++++++++++++++ examples/threading_agent_trap.py | 242 ++++++++++++++++++++++++ netsnmpagent.py | 179 ++++++++++++++++++ netsnmpapi.py | 110 +++++++++++ 6 files changed, 1128 insertions(+) create mode 100644 examples/run_simple_agent_trap.sh create mode 100644 examples/run_threading_agent_trap.sh create mode 100644 examples/simple_agent_trap.py create mode 100644 examples/threading_agent_trap.py diff --git a/examples/run_simple_agent_trap.sh b/examples/run_simple_agent_trap.sh new file mode 100644 index 0000000..c0ffc50 --- /dev/null +++ b/examples/run_simple_agent_trap.sh @@ -0,0 +1,166 @@ +# +# python-netsnmpagent simple example agent +# +# Copyright (c) 2013 Pieter Hollants +# Licensed under the GNU Public License (GPL) version 3 +# + +# +# This script makes running simple_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 +# Find path to snmptrapd executable +SNMPTRAPD_BIN="" +for DIR in /usr/local/sbin /usr/sbin +do + if [ -x $DIR/snmptrapd ] ; then + SNMPTRAPD_BIN=$DIR/snmptrapd + break + fi +done +if [ -z "$SNMPTRAPD_BIN" ] ; then + echo "snmptrapd 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 + # Terminate snmptrapd, if running + if [ -n "$SNMPTRAPD_PIDFILE" -a -e "$SNMPTRAPD_PIDFILE" ] ; then + PID="$(cat $SNMPTRAPD_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 KILL INT HUP + + +# Create a temporary directory +TMPDIR="$(mktemp --directory --tmpdir simple_agent.XXXXXXXXXX)" +SNMPD_CONFFILE=$TMPDIR/snmpd.conf +SNMPD_PIDFILE=$TMPDIR/snmpd.pid +AGENTX_SOCK=$TMPDIR/snmpd-agentx.sock + +# 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 $AGENTX_SOCK + +trapcommunity public +trapsink localhost:5558 +trap2sink localhost:5559 +informsink localhost:5560 + +[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 -M+./ -C -c$SNMPD_CONFFILE -p$SNMPD_PIDFILE + +echo "* Preparing snmptrapd environment..." + +SNMPTRAPD_CONFFILE=$TMPDIR/snmptrapd.conf +SNMPTRAPD_PIDFILE=$TMPDIR/snmptrapd.pid +SNMPTRAPD_HANDLE=$TMPDIR/snmptrapd.handle + +# Create a minimal snmptrapd configuration for our purposes +cat <>$SNMPTRAPD_CONFFILE +snmpTrapdAddr localhost:5558,localhost:5559,localhost:5560 + +doNotRetainNotificationLogs yes + +authCommunity log,execute,net public +#authUser log,execute,net simpleUser noauth + +disableAuthorization yes + +traphandle default $SNMPTRAPD_HANDLE +EOF + +cat <>$SNMPTRAPD_HANDLE +#!/bin/sh + +PATH=/sbin:/usr/sbin:/bin:/usr/bin + +read host +read ip + +logger -t \${0##*/} "host:\$host ip:\$ip" + +while read oid val +do + logger -t \${0##*/} "\$oid \$val" +done +EOF +chmod a+x "$SNMPTRAPD_HANDLE" + +# Start a snmptrapd instance for testing purposes, run as the current user and +# and independent from any other running snmptrapd instance +$SNMPTRAPD_BIN -LE warning -M+./ -C -c$SNMPTRAPD_CONFFILE -p$SNMPTRAPD_PIDFILE -x$AGENTX_SOCK + +# 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 SIMPLE-MIB::simpleMIB" +echo " snmptable -v 2c -c public -M+. -Ci localhost:5555 SIMPLE-MIB::firstTable" +echo " snmpget -v 2c -c public -M+. localhost:5555 SIMPLE-MIB::simpleInteger.0" +echo " snmpset -v 2c -c simple -M+. localhost:5555 SIMPLE-MIB::simpleInteger.0 i 123" +echo "" + +# Workaround to have CTRL-C not generate any visual feedback (we don't do any +# input anyway) +#(ant)stty -echo + +# Now start the simple example agent +echo "* Starting the simple example agent..." +python simple_agent_trap.py -m $AGENTX_SOCK -p $TMPDIR/ + +# Debug mode +#gdb python -ex "run simple_agent_trap.py -m $AGENTX_SOCK -p $TMPDIR/" diff --git a/examples/run_threading_agent_trap.sh b/examples/run_threading_agent_trap.sh new file mode 100644 index 0000000..7757a50 --- /dev/null +++ b/examples/run_threading_agent_trap.sh @@ -0,0 +1,167 @@ +# +# python-netsnmpagent example agent with threading +# +# Copyright (c) 2013 Pieter Hollants +# Licensed under the GNU Public License (GPL) 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 + +# Find path to snmptrapd executable +SNMPTRAPD_BIN="" +for DIR in /usr/local/sbin /usr/sbin +do + if [ -x $DIR/snmptrapd ] ; then + SNMPTRAPD_BIN=$DIR/snmptrapd + break + fi +done +if [ -z "$SNMPTRAPD_BIN" ] ; then + echo "snmptrapd 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 + # Terminate snmptrapd, if running + if [ -n "$SNMPTRAPD_PIDFILE" -a -e "$SNMPTRAPD_PIDFILE" ] ; then + PID="$(cat $SNMPTRAPD_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 KILL 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 +AGENTX_SOCK=$TMPDIR/snmpd-agentx.sock + +# 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 $AGENTX_SOCK + +trapcommunity public +trapsink localhost:5558 +trap2sink localhost:5559 +informsink localhost:5560 + +[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 -M+./ -C -c$SNMPD_CONFFILE -p$SNMPD_PIDFILE + +echo "* Preparing snmptrapd environment..." + +SNMPTRAPD_CONFFILE=$TMPDIR/snmptrapd.conf +SNMPTRAPD_PIDFILE=$TMPDIR/snmptrapd.pid +SNMPTRAPD_HANDLE=$TMPDIR/snmptrapd.handle + +# Create a minimal snmptrapd configuration for our purposes +cat <>$SNMPTRAPD_CONFFILE +snmpTrapdAddr localhost:5558,localhost:5559,localhost:5560 + +doNotRetainNotificationLogs yes + +authCommunity log,execute,net public +#authUser log,execute,net simpleUser noauth + +disableAuthorization yes + +traphandle default $SNMPTRAPD_HANDLE +EOF + +cat <>$SNMPTRAPD_HANDLE +#!/bin/sh + +PATH=/sbin:/usr/sbin:/bin:/usr/bin + +read host +read ip + +logger -t \${0##*/} "host:\$host ip:\$ip" + +while read oid val +do + logger -t \${0##*/} "\$oid \$val" +done +EOF +chmod a+x "$SNMPTRAPD_HANDLE" + +# Start a snmptrapd instance for testing purposes, run as the current user +# and independent from any other running snmptrapd instance +$SNMPTRAPD_BIN -LE warning -M+./ -C -c$SNMPTRAPD_CONFFILE -p$SNMPTRAPD_PIDFILE -x$AGENTX_SOCK + +# 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-MIB::threadingMIB" +echo " snmpget -v 2c -c public -M+. localhost:5555 THREADING-MIB::threadingString.0" +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_agent_trap.py -m $AGENTX_SOCK -p $TMPDIR/ + +# Debug mode +# Remember to comment out 'stty -echo' few lines above +#gdb python -ex "run threading_agent_trap.py -m $AGENTX_SOCK -p $TMPDIR/" diff --git a/examples/simple_agent_trap.py b/examples/simple_agent_trap.py new file mode 100644 index 0000000..6cf7a61 --- /dev/null +++ b/examples/simple_agent_trap.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# +# python-netsnmpagent simple example agent +# +# Copyright (c) 2013 Pieter Hollants +# Licensed under the GNU Public License (GPL) version 3 +# + +# +# This is an example of a simple SNMP sub-agent using the AgentX protocol +# to connect to a master agent (snmpd), extending its MIB with the +# information from the included SIMPLE-MIB.txt. +# +# Use the included script run_simple_agent.sh to test this example. +# +# Alternatively, if you want to test with your system-wide snmpd instance, +# it must have as minimal configuration: +# +# rocommunity 127.0.0.1 +# master agentx +# +# snmpd must be started first, then this agent must be started as root +# (because of the AgentX socket under /var/run/agentx/master), eg. via "sudo". +# +# Then, from a separate console and from inside the python-netsnmpagent +# directory, you can run eg.: +# +# snmpwalk -v 2c -c -M+. localhost SIMPLE-MIB::simpleMIB +# +# If you wish to test setting values as well, your snmpd.conf needs a +# line like this: +# +# rwcommunity 127.0.0.1 +# +# Then you can try something like: +# +# snmpset -v 2c -c -M+. localhost \ +# SIMPLE-MIB::simpleInteger i 0 +# + +import sys, os, signal +import optparse +import pprint + +# Make sure we use the local copy, not a system-wide one +sys.path.insert(0, os.path.dirname(os.getcwd())) +import netsnmpagent + +prgname = sys.argv[0] + +# Process command line arguments +parser = optparse.OptionParser() +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() + +# Get terminal width for usage with pprint +rows,columns = os.popen("stty size", "r").read().split() + +# First, create an instance of the netsnmpAgent class. We specify the +# fully-qualified path to SIMPLE-MIB.txt ourselves here, so that you +# don't have to copy the MIB to /usr/share/snmp/mibs. +try: + agent = netsnmpagent.netsnmpAgent( + AgentName = "SimpleAgent", + MasterSocket = options.mastersocket, + PersistenceDir = options.persistencedir, + MIBFiles = [ os.path.abspath(os.path.dirname(sys.argv[0])) + + "/SIMPLE-MIB.txt" ] + ) +except netsnmpagent.netsnmpAgentException as e: + print "{0}: {1}".format(prgname, e) + sys.exit(1) + +# Then we create all SNMP scalar variables we're willing to serve. +simpleInteger = agent.Integer32( + oidstr = "SIMPLE-MIB::simpleInteger" +) +simpleIntegerContext1 = agent.Integer32( + oidstr = "SIMPLE-MIB::simpleInteger", + context = "context1", + initval = 200, +) +simpleIntegerRO = agent.Integer32( + oidstr = "SIMPLE-MIB::simpleIntegerRO", + writable = False +) +simpleUnsigned = agent.Unsigned32( + oidstr = "SIMPLE-MIB::simpleUnsigned" +) +simpleUnsignedRO = agent.Unsigned32( + oidstr = "SIMPLE-MIB::simpleUnsignedRO", + writable = False +) +simpleCounter32 = agent.Counter32( + oidstr = "SIMPLE-MIB::simpleCounter32" +) +simpleCounter32Context2 = agent.Counter32( + oidstr = "SIMPLE-MIB::simpleCounter32", + context = "context2", + initval = pow(2,32) - 10, # To rule out endianness bugs +) +simpleCounter64 = agent.Counter64( + oidstr = "SIMPLE-MIB::simpleCounter64" +) +simpleCounter64Context2 = agent.Counter64( + oidstr = "SIMPLE-MIB::simpleCounter64", + context = "context2", + initval = pow(2,64) - 10, # To rule out endianness bugs +) +simpleTimeTicks = agent.TimeTicks( + oidstr = "SIMPLE-MIB::simpleTimeTicks" +) +simpleIpAddress = agent.IpAddress( + oidstr = "SIMPLE-MIB::simpleIpAddress", + initval="127.0.0.1" +) +simpleOctetString = agent.OctetString( + oidstr = "SIMPLE-MIB::simpleOctetString", + initval = "Hello World" +) +simpleDisplayString = agent.DisplayString( + oidstr = "SIMPLE-MIB::simpleDisplayString", + initval = "Nice to meet you" +) + +# Create the first table +firstTable = agent.Table( + oidstr = "SIMPLE-MIB::firstTable", + indexes = [ + agent.DisplayString() + ], + columns = [ + (2, agent.DisplayString("Unknown place")), + (3, agent.Integer32(0)) + ], + counterobj = agent.Unsigned32( + oidstr = "SIMPLE-MIB::firstTableNumber" + ) +) + +# Add the first table row +firstTableRow1 = firstTable.addRow([agent.DisplayString("aa")]) +firstTableRow1.setRowCell(2, agent.DisplayString("Prague")) +firstTableRow1.setRowCell(3, agent.Integer32(20)) + +# Add the second table row +firstTableRow2 = firstTable.addRow([agent.DisplayString("ab")]) +firstTableRow2.setRowCell(2, agent.DisplayString("Barcelona")) +firstTableRow2.setRowCell(3, agent.Integer32(28)) + +# Add the third table row +firstTableRow3 = firstTable.addRow([agent.DisplayString("bb")]) +firstTableRow3.setRowCell(3, agent.Integer32(18)) + +# Create the second table +secondTable = agent.Table( + oidstr = "SIMPLE-MIB::secondTable", + indexes = [ + agent.Integer32() + ], + columns = [ + (2, agent.DisplayString("Unknown interface")), + (3, agent.Unsigned32()) + ], + counterobj = agent.Unsigned32( + oidstr = "SIMPLE-MIB::secondTableNumber" + ) +) + +# Add the first table row +secondTableRow1 = secondTable.addRow([agent.Integer32(1)]) +secondTableRow1.setRowCell(2, agent.DisplayString("foo0")) +secondTableRow1.setRowCell(3, agent.Unsigned32(5030)) + +# Add the second table row +secondTableRow2 = secondTable.addRow([agent.Integer32(2)]) +secondTableRow2.setRowCell(2, agent.DisplayString("foo1")) +secondTableRow2.setRowCell(3, agent.Unsigned32(12842)) + +# Finally, we tell the agent to "start". This actually connects the +# agent to the master agent. +try: + agent.start() +except netsnmpagent.netsnmpAgentException as e: + print "{0}: {1}".format(prgname, e) + sys.exit(1) + +print "{0}: AgentX connection to snmpd established.".format(prgname) + +# Helper function that dumps the state of all registered SNMP variables +def DumpRegistered(): + for context in agent.getContexts(): + print "{0}: Registered SNMP objects in Context \"{1}\": ".format(prgname, context) + vars = agent.getRegistered(context) + pprint.pprint(vars, width=columns) + print +DumpRegistered() + +# Install a signal handler that terminates our simple 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) + +# Install a signal handler that dumps the state of all registered values +# when SIGHUP is received +def HupHandler(signum, frame): + DumpRegistered() +signal.signal(signal.SIGHUP, HupHandler) + +# The simple agent's main loop. We loop endlessly until our signal +# handler above changes the "loop" variable. +print "{0}: Serving SNMP requests, send SIGHUP to dump SNMP object state, press ^C to terminate...".format(prgname) + +sendTrap = True +loop = True +while (loop): + # Block and process SNMP requests, if available + agent.check_and_process() + + # Since we didn't give simpleCounter, simpleCounter64 and simpleTimeTicks + # a real meaning in the SIMPLE-MIB, we can basically do with them whatever + # we want. Here, we just increase them, although in different manners. + simpleCounter32.update(simpleCounter32.value() + 2) + simpleCounter64.update(simpleCounter64.value() + 4294967294) + simpleTimeTicks.update(simpleTimeTicks.value() + 1) + + # With counters, you can also call increment() on them + simpleCounter32Context2.increment() # By 1 + simpleCounter64Context2.increment(5) # By 5 + + # agent.check_and_process() is trigering when trap is send so to disable endless loop + # there is flag set to skip sending trap on next check + if sendTrap: + trapOid = 'SNMPv2-MIB::sysDescr' + trapData = [ + { 'oid':'SNMPv2-MIB::sysDescr.0', 'val':'test state' }, + { 'oid':'SIMPLE-MIB::simpleCounter64.0', 'val':1234567890L }, + ] + agent.send_trap( + oid = trapOid, + traps = trapData, + ) + sendTrap = False + else: + sendTrap = True + +print "{0}: Terminating.".format(prgname) +agent.shutdown() diff --git a/examples/threading_agent_trap.py b/examples/threading_agent_trap.py new file mode 100644 index 0000000..a5a9b34 --- /dev/null +++ b/examples/threading_agent_trap.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# +# python-netsnmpagent example agent with threading +# +# Copyright (c) 2013 Pieter Hollants +# Licensed under the GNU Public License (GPL) 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 + +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)) + +# 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-MIB.txt" ], + LogHandler = LogNetSnmpMsg, + ) +except netsnmpagent.netsnmpAgentException as e: + print "{0}: {1}".format(prgname, e) + sys.exit(1) + +# Register the only SNMP object we server, a DisplayString +threadingString = agent.DisplayString( + oidstr = "THREADING-MIB::threadingString", + initval = "" +) + +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 5; date\".") + proc = subprocess.Popen( + "sleep 5; 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) + + # send trap + global agent + trapOid = 'SNMPv2-MIB::sysDescr' + trapData = [ + { 'oid':'SNMPv2-MIB::sysDescr.0', 'val':'test threading agent trap' }, + ] + agent.send_trap( + oid = trapOid, + traps = trapData, + ) + + 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 dc0f8dc..5479fe9 100644 --- a/netsnmpagent.py +++ b/netsnmpagent.py @@ -924,5 +924,184 @@ def shutdown(self): # one effectively has to rely on the OS to release resources. #libnsa.shutdown_agent() + def send_trap(self, *args, **kwargs): + '''Send SNMP traps + To send simple trap: + send_trap(,) + send_trap(trap=,specific=) + + Trap data format: + trapData = [ {'oid':,'var':,'type':}, ] + + By default net-snmp lib add sysUpTime.instance with agent uptime. + Optional add 'uptime = ' to send_trap to use other uptime value + + To send V2 trap: + send_trap( + oid = , + traps = trapData + ) + To send V3 trap: + send_trap( + oid = , + traps = trapData , + context = + ) + ''' + + agent = self + + class snmp_pdu(object): + """ clas for handling SNMP PDU objects """ + + pdu = None + + def __init__(self, pduType = SNMP_MSG_TRAP2): + """create PDU object """ + self.pdu = libnsX.snmp_pdu_create(SNMP_MSG_TRAP2) + + def __del__(self): + """ destructor """ + if self.pdu: + self.free() + + def free(self): + """ free PDU struct """ + if self.pdu: + libnsX.snmp_free_pdu(self.pdu) + self.pdu = None + + def variables(self): + """ function variables() + + return netsnmp_variable_list pointer from PDU + """ + return self.pdu.contents.variables + + def _humanToASNtype(self, varType): + """ convert ASN type name to compatible for snmp_add_var() + type char + return single char + """ + if varType == None: + varType = '=' + if len(varType) > 1: + varTypeL = varType.lower() + if varTypeL[0:3] == 'hex': + varType = 'x' + elif varTypeL[0:3] in ('obj','oid'): + varType = 'o' + elif varTypeL[0:4] == 'uinteger': + varType = '3' + elif varTypeL in ('gauge','unsigned32'): + varType = 'u' + elif varTypeL[0:2] == 'ip': + varType = 'a' + elif varTypeL[0:3] == 'dec': + varType = 'd' + elif varTypeL == 'counter64': + varType = 'C' + elif varTypeL in ('integer','integer32'): + varType = 'i' + elif varType[0] == 'B': + varType = 'b' + return varType[0] + + def add(self, varOID, varData, varType = '='): + """add OID value to PDU list + varType can be any of the following chars:" + i for INTEGER, INTEGER32 + u for UNSIGNED, GAUGE + 3 for UINTEGER + c for COUNTER, COUNTER32 + C for COUNTER64 + s for STRING,OCTET_STR + x for HEX STRING + d for DECIMAL STRING + n for NULLOBJ + o for OBJID + t for TIMETICKS + a for IPADDRESS + b for BITS + = undocumented autodetect feature to get MIB 'SYNTAX' definition + """ + + varType = self._humanToASNtype(varType) + + (varOid, varOidLen) = agent._prepareOID(varOID) + ret = 255 + while ret: + ret = libnsX.snmp_add_var( + self.pdu, + ctypes.cast(ctypes.byref(varOid), c_oid_p), + varOidLen.value, + ctypes.c_char(varType), + ctypes.c_char_p('{0}'.format(varData)) + ) + if ret != 0: + if varType != '=': + varType = '=' + print "ZDBG: ret={0}".format(ret) + else: + break + + return ret + + trap = kwargs.get('trap') + specific = kwargs.get('specific') + oid = kwargs.get('oid') + traps = kwargs.get('traps') + context = kwargs.get('context') + uptime = kwargs.get('uptime') + + if oid: + # send itrap SNMPv2 or SNMPv3 + pdu = snmp_pdu() + + if uptime: + pdu.add('SNMPv2-MIB::sysUpTime.0', uptime, 't') + + result = pdu.add('SNMPv2-MIB::snmpTrapOID.0', oid) + if result != 0: + msg = "Failed to add {0} as snmpTrapOID!".format(oid) + raise netsnmpAgentException(msg) + else: + # add traps + if traps == None: + traps = [] + for entry in traps: + if entry.has_key('oid'): + varOid = entry['oid'] + if entry.has_key('val'): + varData = entry['val'] + varType = None + if entry.has_key('type'): + varType = entry['type'] + pdu.add(varOid, varData, varType) + else: + msg = "missing 'val' key in trap list!" + raise netsnmpAgentException(msg) + else: + msg = "missing 'oid' key in trap list!" + raise netsnmpAgentException(msg) + + variables = pdu.variables() + if context: + # SNMPv3 trap have context + context = ctype.c_char_p(context) + libnsa.send_v3trap(variables, context) + else: + # SNMPv2 trap + libnsa.send_v2trap(variables) + else: + # send easy trap + if trap and specific: + trap = ctypes.c_int(trap) + specific = ctypes.c_int(specific) + elif len(args) == 2 and type(args[0]) == int and type(args[1]) == int: + trap = ctypes.c_int(args[0]) + specific = ctypes.c_int(args[1]) + libnsa.send_easy_trap(trap, specific) + class netsnmpAgentException(Exception): pass diff --git a/netsnmpapi.py b/netsnmpapi.py index 2fe7473..9c132c0 100644 --- a/netsnmpapi.py +++ b/netsnmpapi.py @@ -204,7 +204,9 @@ class netsnmp_handler_registration(ctypes.Structure): pass # include/net-snmp/library/asn1.h ASN_INTEGER = 0x02 ASN_OCTET_STR = 0x04 +ASN_CONSTRUCTOR = 0x20 ASN_APPLICATION = 0x40 +ASN_CONTEXT = 0x80 # counter64 requires some extra work because it can't be reliably represented # by a single C data type @@ -415,3 +417,111 @@ class netsnmp_table_data_set(ctypes.Structure): pass ctypes.c_int # int block ] f.restype = ctypes.c_int + +# include/net-snmp/agent/agent_trap.h +# void send_easy_trap(int trap, int specific); +for f in [ libnsa.send_easy_trap ]: + f.argtypes = [ + ctypes.c_int, # int trap + ctypes.c_int # int specific + ] + f.restype = None # void +# void send_v2trap(netsnmp_variable_list *vars); +for f in [ libnsa.send_v2trap ]: + f.argtypes = [ + netsnmp_variable_list_p # netsnmp_variable_list *vars + ] + f.restype = None # void +# void send_v3trap(netsnmp_variable_list *vars, char *context); +for f in [ libnsa.send_v3trap ]: + f.argtypes = [ + netsnmp_variable_list_p, # netsnmp_variable_list *vars + ctypes.c_char_p # char *context + ] + f.restype = None # void + +# pdu definition +c_ipaddr = (ctypes.c_ubyte * 4) +c_ubyte_p = ctypes.POINTER(ctypes.c_ubyte) + +# include/net-snmp/types.h +class netsnmp_pdu(ctypes.Structure): pass +netsnmp_pdu_p = ctypes.POINTER(netsnmp_pdu) +netsnmp_pdu._fields_ = [ + ("version", ctypes.c_long), # snmp version + ("command", ctypes.c_int), # Type of this PDU + ("reqid", ctypes.c_long), # Request id + ("msgid", ctypes.c_long), # Message id for V3 messages + ("transid", ctypes.c_long), # Unique ID for incoming transactions + ("sessid", ctypes.c_long), # Session id for AgentX messages + ("errstat", ctypes.c_long), # Error status + ("errindex", ctypes.c_long), # Error index + ("time", ctypes.c_ulong), # uptime + ("flags", ctypes.c_ulong), # + ("securityModel", ctypes.c_int), # + # noAuthNoPriv, authNoPriv, authPriv + ("securityLevel", ctypes.c_int), # + ("msgParseModel", ctypes.c_int), # + # Transport-specific opaque data. This replaces the IP-centric address + ("transport_data", ctypes.c_void_p ), # + ("transport_data_length", ctypes.c_int ), # + # The actual transport domain. This SHOULD NOT BE FREE()D. + ("tDomain", c_oid_p), # + ("tDomainLen", ctypes.c_size_t ), # + ("variables", netsnmp_variable_list_p ), # + # SNMPv1 & SNMPv2c fields + ("community", c_ubyte_p ), # community for outgoing requests. + ("community_len", ctypes.c_size_t ), # + # Trap information + ("enterprise", c_oid_p), # System OID + ("enterprise_length", ctypes.c_size_t ), # + ("trap_type", ctypes.c_long ), # trap type + ("specific_type", ctypes.c_long ), # specific type + ("agent address", c_ipaddr ), # This is ONLY used for v1 TRAPs + # SNMPv3 fields + ("contextEngineID", c_ubyte_p ), # context snmpEngineID + ("contextEngineIDLen", ctypes.c_size_t ), # Length of contextEngineID + ("contextName", ctypes.c_char_p), # authoritative contextName + ("contextNameLen", ctypes.c_size_t ), # Length of contextName + ("securityEngineID", c_ubyte_p ), # authoritative snmpEngineID for security + ("securityEngineIDLen", ctypes.c_size_t ), # Length of securityEngineID + ("securityName", ctypes.c_char_p ), # on behalf of this principal + ("securityNameLen", ctypes.c_size_t ), # Length of securityName + # AgentX fields (also uses SNMPv1 community field) + ("priority", ctypes.c_int ), # + ("range_subid", ctypes.c_int ), # + ("securityStateRef", ctypes.c_void_p), # +] + +# include/net-snmp/snmp.h +SNMP_MSG_TRAP = ASN_CONTEXT | ASN_CONSTRUCTOR | 0x4 +SNMP_MSG_TRAP2 = ASN_CONTEXT | ASN_CONSTRUCTOR | 0x7 + +# include/net-snmp/pdu_api.h +#netsnmp_pdu *snmp_pdu_create(int type); +for f in [ libnsX.snmp_pdu_create ]: + f.argtypes = [ + ctypes.c_int + ] + f.restype = netsnmp_pdu_p + +#void snmp_free_pdu( netsnmp_pdu *pdu); +for f in [ libnsX.snmp_free_pdu ]: + f.argumets = [ + netsnmp_pdu_p + ] + f.restype = None + +# int snmp_add_var(netsnmp_pdu *pdu, +# const oid * name, size_t name_length, char type, const char *value) +for f in [ libnsX.snmp_add_var ]: + f.arguments = [ + netsnmp_pdu_p, # netsnmp_pdu *pdu + c_oid_p, # const oid *name + ctypes.c_size_t, # size_t name_length + ctypes.c_char, # char type('=' to get type from OID tree) + ctypes.c_char_p # const char *value + + ] + f.restype = ctypes.c_int +