diff --git a/examples/run_simple_agent.sh b/examples/run_simple_agent.sh index 61f0e95..a7f76ca 100755 --- a/examples/run_simple_agent.sh +++ b/examples/run_simple_agent.sh @@ -86,6 +86,7 @@ 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 " snmptrapd -f -Lo localhost:5556" echo "" # Workaround to have CTRL-C not generate any visual feedback (we don't do any diff --git a/examples/simple_agent.py b/examples/simple_agent.py index d29210c..348003d 100755 --- a/examples/simple_agent.py +++ b/examples/simple_agent.py @@ -41,6 +41,7 @@ import sys, os, signal import optparse import pprint +import time # Make sure we use the local copy, not a system-wide one sys.path.insert(0, os.path.dirname(os.getcwd())) @@ -264,6 +265,7 @@ def HupHandler(signum, frame): # handler above changes the "loop" variable. print("{0}: Serving SNMP requests, send SIGHUP to dump SNMP object state, press ^C to terminate...".format(prgname)) +timer_start = 0 loop = True while (loop): # Block and process SNMP requests, if available @@ -280,5 +282,16 @@ def HupHandler(signum, frame): simpleCounter32Context2.increment() # By 1 simpleCounter64Context2.increment(5) # By 5 + if time.monotonic() - timer_start >= 20: + timer_start = time.monotonic() + simpleUnsignedRO.update(simpleUnsignedRO.value() + 1) + agent.send_trap( + trap='SIMPLE-MIB::simpleUnsignedROChange', + varlist={ + 'SNMPv2-MIB::sysDescr.0': agent.DisplayString('System'), + 'SIMPLE-MIB::simpleUnsignedRO.0': simpleUnsignedRO, + } + ) + print("{0}: Terminating.".format(prgname)) agent.shutdown() diff --git a/netsnmpagent.py b/netsnmpagent.py index 707bdb1..38279b6 100644 --- a/netsnmpagent.py +++ b/netsnmpagent.py @@ -45,6 +45,26 @@ def enum(*sequential, **named): "RECONNECTING", # Got disconnected, trying to reconnect ) + +class VarList: + def __init__(self): + self.variables = netsnmp_variable_list_p() + + def __del__(self): + libnsa.snmp_free_varbind(self.variables) + + def add_variable(self, name, value): + oid = read_objid(name) + + if not libnsa.snmp_varlist_add_variable( + ctypes.byref(self.variables), + oid, len(oid), + value._asntype, + value.cref(), value._data_size, + ): + raise netsnmpAgentException("snmp_varlist_add_variable() failed!") + + class netsnmpAgent(object): """ Implements an SNMP agent using the net-snmp libraries. """ @@ -389,29 +409,7 @@ def _prepareRegistration(self, oidstr, writable = True): raise netsnmpAgentException("Attempt to register SNMP object " \ "after agent has been started!") - 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 - # read_objid() below - oid = (c_oid * MAX_OID_LEN)() - oid_len = ctypes.c_size_t(MAX_OID_LEN) - - # Let libsnmpagent parse the OID - if libnsa.read_objid( - b(oidstr), - ctypes.cast(ctypes.byref(oid), c_oid_p), - ctypes.byref(oid_len) - ) == 0: - raise netsnmpAgentException("read_objid({0}) failed!".format(oidstr)) - else: - # Interpret the given oidstr as the oid itself. - try: - parts = [c_oid(long(x) if sys.version_info <= (3,) else int(x)) for x in oidstr.split('.')] - except ValueError: - raise netsnmpAgentException("Invalid OID (not using MIB): {0}".format(oidstr)) - - oid = (c_oid * len(parts))(*parts) - oid_len = ctypes.c_size_t(len(parts)) + oid = read_objid(oidstr) # Do we allow SNMP SETting to this OID? handler_modes = HANDLER_CAN_RWRITE if writable \ @@ -425,7 +423,7 @@ def _prepareRegistration(self, oidstr, writable = True): b(oidstr), None, oid, - oid_len, + len(oid), handler_modes ) @@ -740,5 +738,61 @@ def shutdown(self): # one effectively has to rely on the OS to release resources. #libnsa.shutdown_agent() + def send_trap(self, trap, specific=None, varlist=None, context=None, uptime=None): + """ + Send SNMP Trap + + To send SNMPv1 trap + send_trap(, ) + + where and are numbers + + To send SNMPv2 or SNMPv3 trap + send_trap(, varlist=) + + where is OID and varlist is optinal list of variables + + for SNMPv3 trap, also can be specified + + Varlist format is + { + : + } + + where is string representation of variable OID and value is typed value. It can be directly agent + variable or specific variable type instance e.g. + + var_value = agent.TimeTicks(1) + """ + + if isinstance(trap, int): + # send SNMPv1 trap + libnsa.send_easy_trap(ctypes.c_int(trap), ctypes.c_int(specific)) + else: + # send SNMPv2 or SNMPv3 trap + variables = VarList() + + if uptime: + if not isinstance(uptime, netsnmpvartypes.TimeTicks): + uptime = netsnmpvartypes.TimeTicks(uptime) + # SNMPv2-MIB::sysUpTime.0 + variables.add_variable(".1.3.6.1.2.1.1.3.0", uptime) + + # SNMPv2-MIB::snmpTrapOID.0 + variables.add_variable(".1.3.6.1.6.3.1.1.4.1.0", netsnmpvartypes.ObjectId(trap)) + + # add variable list + if varlist: + for var_name, var_value in varlist.items(): + variables.add_variable(var_name, var_value) + + if context: + # SNMPv3 trap have context + libnsa.send_v3trap(variables.variables, b(context)) + else: + # SNMPv2 trap + libnsa.send_v2trap(variables.variables) + + class netsnmpAgentException(Exception): pass diff --git a/netsnmpapi.py b/netsnmpapi.py index ae18275..48bd298 100644 --- a/netsnmpapi.py +++ b/netsnmpapi.py @@ -219,6 +219,7 @@ class netsnmp_handler_registration(ctypes.Structure): pass # include/net-snmp/library/asn1.h ASN_INTEGER = 0x02 ASN_OCTET_STR = 0x04 +ASN_OBJECT_ID = 0x06 ASN_OPAQUE_TAG2 = 0x30 ASN_APPLICATION = 0x40 @@ -438,3 +439,35 @@ class netsnmp_table_data_set(ctypes.Structure): pass ctypes.c_int # int block ] f.restype = ctypes.c_int + + +def read_objid(objid_str): + """ + python wrapper for libnetsnmpagent read_objid function + + Convert string OID array with correct length + """ + + # 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 + # read_objid() below + oid = (c_oid * MAX_OID_LEN)() + oid_len = ctypes.c_size_t(MAX_OID_LEN) + + # Let libsnmpagent parse the OID + if libnsa.read_objid( + b(objid_str), + ctypes.cast(ctypes.byref(oid), c_oid_p), + ctypes.byref(oid_len) + ) == 0: + raise Exception("read_objid({0}) failed!".format(objid_str)) + + # trim oid array to correct length + return (c_oid * oid_len.value)(*(oid[0:oid_len.value])) + + +def format_objid(oid): + """ + Format objid as string + """ + return ".".join([str(o) for o in oid]) diff --git a/netsnmpvartypes.py b/netsnmpvartypes.py index c37199c..6ea6256 100644 --- a/netsnmpvartypes.py +++ b/netsnmpvartypes.py @@ -242,3 +242,37 @@ def value(self): class DisplayString(_String): pass + + +class ObjectId(_VarType): + def __init__(self, initval=""): + self._asntype = ASN_OBJECT_ID + self._ctype = c_oid_p + self._flags = WATCHER_MAX_SIZE + self._max_size = MAX_OID_LEN * ctypes.sizeof(c_oid) + self._cvar = ctypes.cast(ctypes.create_string_buffer(self._max_size), c_oid_p) + self._data_size = self._max_size + self.update(initval) + + def update(self, val): + if isinstance(val, (list, tuple)): + cvar = (c_oid * len(val))(*val) + elif isinstance(val, ctypes.Array): + cvar = val + else: + cvar = read_objid(val) + + self._data_size = min(ctypes.sizeof(cvar), self._max_size) + if hasattr(self, "_watcher"): + self._watcher.contents.data_size = self._data_size + ctypes.memmove(self._cvar, cvar, self._data_size) + + def value(self): + if hasattr(self, "_watcher"): + size = self._watcher.contents.data_size + else: + size = self._data_size + return format_objid(self._cvar[:(size // ctypes.sizeof(c_oid))]) + + def cref(self, **kwargs): + return self._cvar