1#
2# python-netsnmpagent module
3# Copyright (c) 2013-2016 Pieter Hollants <pieter@hollants.com>
4# Licensed under the GNU Lesser Public License (LGPL) version 3
5#
6# Main module
7#
8
9""" Allows to write net-snmp subagents in Python.
10
11The Python bindings that ship with net-snmp support client operations
12only. I fixed a couple of issues in the existing python-agentx module
13but eventually decided to write a new module from scratch due to design
14issues. For example, it implemented its own handler for registered SNMP
15variables, which requires re-doing a lot of stuff which net-snmp
16actually takes care of in its API's helpers.
17
18This module, by contrast, concentrates on wrapping the net-snmp C API
19for SNMP subagents in an easy manner. """
20
21import sys, os, socket, struct, re, locale
22from collections import defaultdict
23from netsnmpapi import *
24
25# Maximum string size supported by python-netsnmpagent
26MAX_STRING_SIZE = 1024
27
28# Helper function courtesy of Alec Thomas and taken from
29# http://stackoverflow.com/questions/36932/how-can-i-represent-an-enum-in-python
30def enum(*sequential, **named):
31	enums = dict(zip(sequential, range(len(sequential))), **named)
32	try:
33		# Python 2.x
34		enums_iterator = enums.iteritems()
35	except AttributeError:
36		# Python 3.x
37		enums_iterator = enums.items()
38	enums["Names"] = dict((value,key) for key, value in enums_iterator)
39	return type("Enum", (), enums)
40
41# Helper functions to deal with converting between byte strings (required by
42# ctypes) and Unicode strings (possibly used by the Python version in use)
43def b(s):
44	""" Encodes Unicode strings to byte strings, if necessary. """
45
46	return s if isinstance(s, bytes) else s.encode(locale.getpreferredencoding())
47
48def u(s):
49	""" Decodes byte strings to Unicode strings, if necessary. """
50
51	return s if isinstance("Test", bytes) else s.decode(locale.getpreferredencoding())
52
53# Indicates the status of a netsnmpAgent object
54netsnmpAgentStatus = enum(
55	"REGISTRATION",     # Unconnected, SNMP object registrations possible
56	"FIRSTCONNECT",     # No more registrations, first connection attempt
57	"CONNECTFAILED",    # Error connecting to snmpd
58	"CONNECTED",        # Connected to a running snmpd instance
59	"RECONNECTING",     # Got disconnected, trying to reconnect
60)
61
62# Helper function to determine if "x" is a num
63def isnum(x):
64	try:
65		x + 1
66		return True
67	except TypeError:
68		return False
69
70class netsnmpAgent(object):
71	""" Implements an SNMP agent using the net-snmp libraries. """
72
73	def __init__(self, **args):
74		"""Initializes a new netsnmpAgent instance.
75
76		"args" is a dictionary that can contain the following
77		optional parameters:
78
79		- AgentName     : The agent's name used for registration with net-snmp.
80		- MasterSocket  : The transport specification of the AgentX socket of
81		                  the running snmpd instance to connect to (see the
82		                  "LISTENING ADDRESSES" section in the snmpd(8) manpage).
83		                  Change this if you want to use eg. a TCP transport or
84		                  access a custom snmpd instance, eg. as shown in
85		                  run_simple_agent.sh, or for automatic testing.
86		- PersistenceDir: The directory to use to store persistence information.
87		                  Change this if you want to use a custom snmpd
88		                  instance, eg. for automatic testing.
89		- MIBFiles      : A list of filenames of MIBs to be loaded. Required if
90		                  the OIDs, for which variables will be registered, do
91		                  not belong to standard MIBs and the custom MIBs are not
92		                  located in net-snmp's default MIB path
93		                  (/usr/share/snmp/mibs).
94		- UseMIBFiles   : Whether to use MIB files at all or not. When False,
95		                  the parser for MIB files will not be initialized, so
96		                  neither system-wide MIB files nor the ones provided
97		                  in the MIBFiles argument will be in use.
98		- LogHandler    : An optional Python function that will be registered
99		                  with net-snmp as a custom log handler. If specified,
100		                  this function will be called for every log message
101		                  net-snmp itself generates, with parameters as follows:
102		                  1. a string indicating the message's priority: one of
103		                  "Emergency", "Alert", "Critical", "Error", "Warning",
104		                  "Notice", "Info" or "Debug".
105		                  2. the actual log message. Note that heading strings
106		                  such as "Warning: " and "Error: " will be stripped off
107		                  since the priority level is explicitly known and can
108		                  be used to prefix the log message, if desired.
109		                  Trailing linefeeds will also have been stripped off.
110		                  If undefined, log messages will be written to stderr
111		                  instead. """
112
113		# Default settings
114		defaults = {
115			"AgentName"     : os.path.splitext(os.path.basename(sys.argv[0]))[0],
116			"MasterSocket"  : None,
117			"PersistenceDir": None,
118			"UseMIBFiles"   : True,
119			"MIBFiles"      : None,
120			"LogHandler"    : None,
121		}
122		for key in defaults:
123			setattr(self, key, args.get(key, defaults[key]))
124		if self.UseMIBFiles and self.MIBFiles is not None and type(self.MIBFiles) not in (list, tuple):
125			self.MIBFiles = (self.MIBFiles,)
126
127		# Initialize status attribute -- until start() is called we will accept
128		# SNMP object registrations
129		self._status = netsnmpAgentStatus.REGISTRATION
130
131		# Unfortunately net-snmp does not give callers of init_snmp() (used
132		# in the start() method) any feedback about success or failure of
133		# connection establishment. But for AgentX clients this information is
134		# quite essential, thus we need to implement some more or less ugly
135		# workarounds.
136
137		# For net-snmp 5.7.x, we can derive success and failure from the log
138		# messages it generates. Normally these go to stderr, in the absence
139		# of other so-called log handlers. Alas we define a callback function
140		# that we will register with net-snmp as a custom log handler later on,
141		# hereby effectively gaining access to the desired information.
142		def _py_log_handler(majorID, minorID, serverarg, clientarg):
143			# "majorID" and "minorID" are the callback IDs with which this
144			# callback function was registered. They are useful if the same
145			# callback was registered multiple times.
146			# Both "serverarg" and "clientarg" are pointers that can be used to
147			# convey information from the calling context to the callback
148			# function: "serverarg" gets passed individually to every call of
149			# snmp_call_callbacks() while "clientarg" was initially passed to
150			# snmp_register_callback().
151
152			# In this case, "majorID" and "minorID" are always the same (see the
153			# registration code below). "serverarg" needs to be cast back to
154			# become a pointer to a "snmp_log_message" C structure (passed by
155			# net-snmp's log_handler_callback() in snmplib/snmp_logging.c) while
156			# "clientarg" will be None (see the registration code below).
157			logmsg = ctypes.cast(serverarg, snmp_log_message_p)
158
159			# Generate textual description of priority level
160			priorities = {
161				LOG_EMERG: "Emergency",
162				LOG_ALERT: "Alert",
163				LOG_CRIT: "Critical",
164				LOG_ERR: "Error",
165				LOG_WARNING: "Warning",
166				LOG_NOTICE: "Notice",
167				LOG_INFO: "Info",
168				LOG_DEBUG: "Debug"
169			}
170			msgprio = priorities[logmsg.contents.priority]
171
172			# Strip trailing linefeeds and in addition "Warning: " and "Error: "
173			# from msgtext as these conditions are already indicated through
174			# msgprio
175			msgtext = re.sub(
176				"^(Warning|Error): *",
177				"",
178				u(logmsg.contents.msg.rstrip(b"\n"))
179			)
180
181			# Intercept log messages related to connection establishment and
182			# failure to update the status of this netsnmpAgent object. This is
183			# really an ugly hack, introducing a dependency on the particular
184			# text of log messages -- hopefully the net-snmp guys won't
185			# translate them one day.
186			if  msgprio == "Warning" \
187			or  msgprio == "Error" \
188			and re.match("Failed to .* the agentx master agent.*", msgtext):
189				# If this was the first connection attempt, we consider the
190				# condition fatal: it is more likely that an invalid
191				# "MasterSocket" was specified than that we've got concurrency
192				# issues with our agent being erroneously started before snmpd.
193				if self._status == netsnmpAgentStatus.FIRSTCONNECT:
194					self._status = netsnmpAgentStatus.CONNECTFAILED
195
196					# No need to log this message -- we'll generate our own when
197					# throwing a netsnmpAgentException as consequence of the
198					# ECONNECT
199					return 0
200
201				# Otherwise we'll stay at status RECONNECTING and log net-snmp's
202				# message like any other. net-snmp code will keep retrying to
203				# connect.
204			elif msgprio == "Info" \
205			and  re.match("AgentX subagent connected", msgtext):
206				self._status = netsnmpAgentStatus.CONNECTED
207			elif msgprio == "Info" \
208			and  re.match("AgentX master disconnected us.*", msgtext):
209				self._status = netsnmpAgentStatus.RECONNECTING
210
211			# If "LogHandler" was defined, call it to take care of logging.
212			# Otherwise print all log messages to stderr to resemble net-snmp
213			# standard behavior (but add log message's associated priority in
214			# plain text as well)
215			if self.LogHandler:
216				self.LogHandler(msgprio, msgtext)
217			else:
218				print("[{0}] {1}".format(msgprio, msgtext))
219
220			return 0
221
222		# We defined a Python function that needs a ctypes conversion so it can
223		# be called by C code such as net-snmp. That's what SNMPCallback() is
224		# used for. However we also need to store the reference in "self" as it
225		# will otherwise be lost at the exit of this function so that net-snmp's
226		# attempt to call it would end in nirvana...
227		self._log_handler = SNMPCallback(_py_log_handler)
228
229		# Now register our custom log handler with majorID SNMP_CALLBACK_LIBRARY
230		# and minorID SNMP_CALLBACK_LOGGING.
231		if libnsa.snmp_register_callback(
232			SNMP_CALLBACK_LIBRARY,
233			SNMP_CALLBACK_LOGGING,
234			self._log_handler,
235			None
236		) != SNMPERR_SUCCESS:
237			raise netsnmpAgentException(
238				"snmp_register_callback() failed for _netsnmp_log_handler!"
239			)
240
241		# Finally the net-snmp logging system needs to be told to enable
242		# logging through callback functions. This will actually register a
243		# NETSNMP_LOGHANDLER_CALLBACK log handler that will call out to any
244		# callback functions with the majorID and minorID shown above, such as
245		# ours.
246		libnsa.snmp_enable_calllog()
247
248		# Unfortunately our custom log handler above is still not enough: in
249		# net-snmp 5.4.x there were no "AgentX master disconnected" log
250		# messages yet. So we need another workaround to be able to detect
251		# disconnects for this release. Both net-snmp 5.4.x and 5.7.x support
252		# a callback mechanism using the "majorID" SNMP_CALLBACK_APPLICATION and
253		# the "minorID" SNMPD_CALLBACK_INDEX_STOP, which we can abuse for our
254		# purposes. Again, we start by defining a callback function.
255		def _py_index_stop_callback(majorID, minorID, serverarg, clientarg):
256			# For "majorID" and "minorID" see our log handler above.
257			# "serverarg" is a disguised pointer to a "netsnmp_session"
258			# structure (passed by net-snmp's subagent_open_master_session() and
259			# agentx_check_session() in agent/mibgroup/agentx/subagent.c). We
260			# can ignore it here since we have a single session only anyway.
261			# "clientarg" will be None again (see the registration code below).
262
263			# We only care about SNMPD_CALLBACK_INDEX_STOP as our custom log
264			# handler above already took care of all other events.
265			if minorID == SNMPD_CALLBACK_INDEX_STOP:
266				self._status = netsnmpAgentStatus.RECONNECTING
267
268			return 0
269
270		# Convert it to a C callable function and store its reference
271		self._index_stop_callback = SNMPCallback(_py_index_stop_callback)
272
273		# Register it with net-snmp
274		if libnsa.snmp_register_callback(
275			SNMP_CALLBACK_APPLICATION,
276			SNMPD_CALLBACK_INDEX_STOP,
277			self._index_stop_callback,
278			None
279		) != SNMPERR_SUCCESS:
280			raise netsnmpAgentException(
281				"snmp_register_callback() failed for _netsnmp_index_callback!"
282			)
283
284		# No enabling necessary here
285
286		# Make us an AgentX client
287		if libnsa.netsnmp_ds_set_boolean(
288			NETSNMP_DS_APPLICATION_ID,
289			NETSNMP_DS_AGENT_ROLE,
290			1
291		) != SNMPERR_SUCCESS:
292			raise netsnmpAgentException(
293				"netsnmp_ds_set_boolean() failed for NETSNMP_DS_AGENT_ROLE!"
294			)
295
296		# Use an alternative transport specification to connect to the master?
297		# Defaults to "/var/run/agentx/master".
298		# (See the "LISTENING ADDRESSES" section in the snmpd(8) manpage)
299		if self.MasterSocket:
300			if libnsa.netsnmp_ds_set_string(
301				NETSNMP_DS_APPLICATION_ID,
302				NETSNMP_DS_AGENT_X_SOCKET,
303				b(self.MasterSocket)
304			) != SNMPERR_SUCCESS:
305				raise netsnmpAgentException(
306					"netsnmp_ds_set_string() failed for NETSNMP_DS_AGENT_X_SOCKET!"
307				)
308
309		# Use an alternative persistence directory?
310		if self.PersistenceDir:
311			if libnsa.netsnmp_ds_set_string(
312				NETSNMP_DS_LIBRARY_ID,
313				NETSNMP_DS_LIB_PERSISTENT_DIR,
314				b(self.PersistenceDir)
315			) != SNMPERR_SUCCESS:
316				raise netsnmpAgentException(
317					"netsnmp_ds_set_string() failed for NETSNMP_DS_LIB_PERSISTENT_DIR!"
318				)
319
320		# Initialize net-snmp library (see netsnmp_agent_api(3))
321		if libnsa.init_agent(b(self.AgentName)) != 0:
322			raise netsnmpAgentException("init_agent() failed!")
323
324		# Initialize MIB parser
325		if self.UseMIBFiles:
326			libnsa.netsnmp_init_mib()
327
328		# If MIBFiles were specified (ie. MIBs that can not be found in
329		# net-snmp's default MIB directory /usr/share/snmp/mibs), read
330		# them in so we can translate OID strings to net-snmp's internal OID
331		# format.
332		if self.UseMIBFiles and self.MIBFiles:
333			for mib in self.MIBFiles:
334				if libnsa.read_mib(b(mib)) == 0:
335					raise netsnmpAgentException("netsnmp_read_module({0}) " +
336					                            "failed!".format(mib))
337
338		# Initialize our SNMP object registry
339		self._objs = defaultdict(dict)
340
341	def _prepareRegistration(self, oidstr, writable = True):
342		""" Prepares the registration of an SNMP object.
343
344		    "oidstr" is the OID to register the object at.
345		    "writable" indicates whether "snmpset" is allowed. """
346
347		# Make sure the agent has not been start()ed yet
348		if self._status != netsnmpAgentStatus.REGISTRATION:
349			raise netsnmpAgentException("Attempt to register SNMP object " \
350			                            "after agent has been started!")
351
352		if self.UseMIBFiles:
353			# We can't know the length of the internal OID representation
354			# beforehand, so we use a MAX_OID_LEN sized buffer for the call to
355			# read_objid() below
356			oid = (c_oid * MAX_OID_LEN)()
357			oid_len = ctypes.c_size_t(MAX_OID_LEN)
358
359			# Let libsnmpagent parse the OID
360			if libnsa.read_objid(
361				b(oidstr),
362				ctypes.cast(ctypes.byref(oid), c_oid_p),
363				ctypes.byref(oid_len)
364			) == 0:
365				raise netsnmpAgentException("read_objid({0}) failed!".format(oidstr))
366		else:
367			# Interpret the given oidstr as the oid itself.
368			try:
369				parts = [c_oid(long(x) if sys.version_info <= (3,) else int(x)) for x in oidstr.split('.')]
370			except ValueError:
371				raise netsnmpAgentException("Invalid OID (not using MIB): {0}".format(oidstr))
372
373			oid = (c_oid * len(parts))(*parts)
374			oid_len = ctypes.c_size_t(len(parts))
375
376		# Do we allow SNMP SETting to this OID?
377		handler_modes = HANDLER_CAN_RWRITE if writable \
378		                                   else HANDLER_CAN_RONLY
379
380		# Create the netsnmp_handler_registration structure. It notifies
381		# net-snmp that we will be responsible for anything below the given
382		# OID. We use this for leaf nodes only, processing of subtrees will be
383		# left to net-snmp.
384		handler_reginfo = libnsa.netsnmp_create_handler_registration(
385			b(oidstr),
386			None,
387			oid,
388			oid_len,
389			handler_modes
390		)
391
392		return handler_reginfo
393
394	def VarTypeClass(property_func):
395		""" Decorator that transforms a simple property_func into a class
396		    factory returning instances of a class for the particular SNMP
397		    variable type. property_func is supposed to return a dictionary with
398		    the following elements:
399		    - "ctype"           : A reference to the ctypes constructor method
400		                          yielding the appropriate C representation of
401		                          the SNMP variable, eg. ctypes.c_long or
402		                          ctypes.create_string_buffer.
403		    - "flags"           : A net-snmp constant describing the C data
404		                          type's storage behavior, currently either
405		                          WATCHER_FIXED_SIZE or WATCHER_MAX_SIZE.
406		    - "max_size"        : The maximum allowed string size if "flags"
407		                          has been set to WATCHER_MAX_SIZE.
408		    - "initval"         : The value to initialize the C data type with,
409		                          eg. 0 or "".
410		    - "asntype"         : A constant defining the SNMP variable type
411		                          from an ASN.1 perspective, eg. ASN_INTEGER.
412		    - "context"         : A string defining the context name for the
413		                          SNMP variable
414
415		    The class instance returned will have no association with net-snmp
416		    yet. Use the Register() method to associate it with an OID. """
417
418		# This is the replacement function, the "decoration"
419		def create_vartype_class(self, initval = None, oidstr = None, writable = True, context = ""):
420			agent = self
421
422			# Call the original property_func to retrieve this variable type's
423			# properties. Passing "initval" to property_func may seem pretty
424			# useless as it won't have any effect and we use it ourselves below.
425			# However we must supply it nevertheless since it's part of
426			# property_func's function signature which THIS function shares.
427			# That's how Python's decorators work.
428			props = property_func(self, initval)
429
430			# Use variable type's default initval if we weren't given one
431			if initval == None:
432				initval = props["initval"]
433
434			# Create a class to wrap ctypes' access semantics and enable
435			# Register() to do class-specific registration work.
436			#
437			# Since the part behind the "class" keyword can't be a variable, we
438			# use the proxy name "cls" and overwrite its __name__ property
439			# after class creation.
440			class cls(object):
441				def __init__(self):
442					for prop in ["flags", "asntype"]:
443						setattr(self, "_{0}".format(prop), props[prop])
444
445					# Create the ctypes class instance representing the variable
446					# to be handled by the net-snmp C API. If this variable type
447					# has no fixed size, pass the maximum size as second
448					# argument to the constructor.
449					if props["flags"] == WATCHER_FIXED_SIZE:
450						self._cvar      = props["ctype"](initval if isnum(initval) else b(initval))
451						self._data_size = ctypes.sizeof(self._cvar)
452						self._max_size  = self._data_size
453					else:
454						self._cvar      = props["ctype"](initval if isnum(initval) else b(initval), props["max_size"])
455						self._data_size = len(self._cvar.value)
456						self._max_size  = max(self._data_size, props["max_size"])
457
458					if oidstr:
459						# Prepare the netsnmp_handler_registration structure.
460						handler_reginfo = agent._prepareRegistration(oidstr, writable)
461						handler_reginfo.contents.contextName = b(context)
462
463						# Create the netsnmp_watcher_info structure.
464						self._watcher = libnsX.netsnmp_create_watcher_info(
465							self.cref(),
466							self._data_size,
467							self._asntype,
468							self._flags
469						)
470
471						# Explicitly set netsnmp_watcher_info structure's
472						# max_size parameter. netsnmp_create_watcher_info6 would
473						# have done that for us but that function was not yet
474						# available in net-snmp 5.4.x.
475						self._watcher.contents.max_size = self._max_size
476
477						# Register handler and watcher with net-snmp.
478						result = libnsX.netsnmp_register_watched_scalar(
479							handler_reginfo,
480							self._watcher
481						)
482						if result != 0:
483							raise netsnmpAgentException("Error registering variable with net-snmp!")
484
485						# Finally, we keep track of all registered SNMP objects for the
486						# getRegistered() method.
487						agent._objs[context][oidstr] = self
488
489				def value(self):
490					val = self._cvar.value
491
492					if isnum(val):
493						# Python 2.x will automatically switch from the "int"
494						# type to the "long" type, if necessary. Python 3.x
495						# has no limits on the "int" type anymore.
496						val = int(val)
497					else:
498						val = u(val)
499
500					return val
501
502				def cref(self, **kwargs):
503					return ctypes.byref(self._cvar) if self._flags == WATCHER_FIXED_SIZE \
504					                                else self._cvar
505
506				def update(self, val):
507					if self._asntype == ASN_COUNTER and val >> 32:
508						val = val & 0xFFFFFFFF
509					if self._asntype == ASN_COUNTER64 and val >> 64:
510						val = val & 0xFFFFFFFFFFFFFFFF
511					self._cvar.value = val
512					if props["flags"] == WATCHER_MAX_SIZE:
513						if len(val) > self._max_size:
514							raise netsnmpAgentException(
515								"Value passed to update() truncated: {0} > {1} "
516								"bytes!".format(len(val), self._max_size)
517							)
518						self._data_size = self._watcher.contents.data_size = len(val)
519
520				if props["asntype"] in [ASN_COUNTER, ASN_COUNTER64]:
521					def increment(self, count=1):
522						self.update(self.value() + count)
523
524			cls.__name__ = property_func.__name__
525
526			# Return an instance of the just-defined class to the agent
527			return cls()
528
529		return create_vartype_class
530
531	@VarTypeClass
532	def Integer32(self, initval = None, oidstr = None, writable = True, context = ""):
533		return {
534			"ctype"         : ctypes.c_long,
535			"flags"         : WATCHER_FIXED_SIZE,
536			"initval"       : 0,
537			"asntype"       : ASN_INTEGER
538		}
539
540	@VarTypeClass
541	def Unsigned32(self, initval = None, oidstr = None, writable = True, context = ""):
542		return {
543			"ctype"         : ctypes.c_ulong,
544			"flags"         : WATCHER_FIXED_SIZE,
545			"initval"       : 0,
546			"asntype"       : ASN_UNSIGNED
547		}
548
549	@VarTypeClass
550	def Counter32(self, initval = None, oidstr = None, writable = True, context = ""):
551		return {
552			"ctype"         : ctypes.c_ulong,
553			"flags"         : WATCHER_FIXED_SIZE,
554			"initval"       : 0,
555			"asntype"       : ASN_COUNTER
556		}
557
558	@VarTypeClass
559	def Counter64(self, initval = None, oidstr = None, writable = True, context = ""):
560		return {
561			"ctype"         : counter64,
562			"flags"         : WATCHER_FIXED_SIZE,
563			"initval"       : 0,
564			"asntype"       : ASN_COUNTER64
565		}
566
567	@VarTypeClass
568	def TimeTicks(self, initval = None, oidstr = None, writable = True, context = ""):
569		return {
570			"ctype"         : ctypes.c_ulong,
571			"flags"         : WATCHER_FIXED_SIZE,
572			"initval"       : 0,
573			"asntype"       : ASN_TIMETICKS
574		}
575
576	# Note we can't use ctypes.c_char_p here since that creates an immutable
577	# type and net-snmp _can_ modify the buffer (unless writable is False).
578	# Also note that while net-snmp 5.5 introduced a WATCHER_SIZE_STRLEN flag,
579	# we have to stick to WATCHER_MAX_SIZE for now to support net-snmp 5.4.x
580	# (used eg. in SLES 11 SP2 and Ubuntu 12.04 LTS).
581	@VarTypeClass
582	def OctetString(self, initval = None, oidstr = None, writable = True, context = ""):
583		return {
584			"ctype"         : ctypes.create_string_buffer,
585			"flags"         : WATCHER_MAX_SIZE,
586			"max_size"      : MAX_STRING_SIZE,
587			"initval"       : "",
588			"asntype"       : ASN_OCTET_STR
589		}
590
591	# Whereas an OctetString can contain UTF-8 encoded characters, a
592	# DisplayString is restricted to ASCII characters only.
593	@VarTypeClass
594	def DisplayString(self, initval = None, oidstr = None, writable = True, context = ""):
595		return {
596			"ctype"         : ctypes.create_string_buffer,
597			"flags"         : WATCHER_MAX_SIZE,
598			"max_size"      : MAX_STRING_SIZE,
599			"initval"       : "",
600			"asntype"       : ASN_OCTET_STR
601		}
602
603	# IP addresses are stored as unsigned integers, but the Python interface
604	# should use strings. So we need a special class.
605	def IpAddress(self, initval = "0.0.0.0", oidstr = None, writable = True, context = ""):
606		agent = self
607
608		class IpAddress(object):
609			def __init__(self):
610				self._flags     = WATCHER_FIXED_SIZE
611				self._asntype   = ASN_IPADDRESS
612				self._cvar      = ctypes.c_uint(0)
613				self._data_size = ctypes.sizeof(self._cvar)
614				self._max_size  = self._data_size
615				self.update(initval)
616
617				if oidstr:
618					# Prepare the netsnmp_handler_registration structure.
619					handler_reginfo = agent._prepareRegistration(oidstr, writable)
620					handler_reginfo.contents.contextName = b(context)
621
622					# Create the netsnmp_watcher_info structure.
623					watcher = libnsX.netsnmp_create_watcher_info(
624						self.cref(),
625						ctypes.sizeof(self._cvar),
626						ASN_IPADDRESS,
627						WATCHER_FIXED_SIZE
628					)
629					watcher._maxsize = ctypes.sizeof(self._cvar)
630
631					# Register handler and watcher with net-snmp.
632					result = libnsX.netsnmp_register_watched_instance(
633						handler_reginfo,
634						watcher
635					)
636					if result != 0:
637						raise netsnmpAgentException("Error registering variable with net-snmp!")
638
639					# Finally, we keep track of all registered SNMP objects for the
640					# getRegistered() method.
641					agent._objs[context][oidstr] = self
642
643			def value(self):
644				# Get string representation of IP address.
645				return socket.inet_ntoa(
646					struct.pack("I", self._cvar.value)
647				)
648
649			def cref(self, **kwargs):
650				# Due to an unfixed Net-SNMP issue (see
651				# https://sourceforge.net/p/net-snmp/bugs/2136/) we have
652				# to convert the value to host byte order if it shall be
653				# used as table index.
654				if kwargs.get("is_table_index", False) == False:
655					return ctypes.byref(self._cvar)
656				else:
657					_cidx = ctypes.c_uint(0)
658					_cidx.value = struct.unpack("I", struct.pack("!I", self._cvar.value))[0]
659					return ctypes.byref(_cidx)
660
661			def update(self, val):
662				# Convert dotted decimal IP address string to ctypes
663				# unsigned int in network byte order.
664				self._cvar.value = struct.unpack(
665					"I",
666					socket.inet_aton(val)
667				)[0]
668
669		# Return an instance of the just-defined class to the agent
670		return IpAddress()
671
672	def Table(self, oidstr, indexes, columns, counterobj = None, extendable = False, context = ""):
673		agent = self
674
675		# Define a Python class to provide access to the table.
676		class Table(object):
677			def __init__(self, oidstr, idxobjs, coldefs, counterobj, extendable, context):
678				# Create a netsnmp_table_data_set structure, representing both
679				# the table definition and the data stored inside it. We use the
680				# oidstr as table name.
681				self._dataset = libnsX.netsnmp_create_table_data_set(
682					ctypes.c_char_p(b(oidstr))
683				)
684
685				# Define the table row's indexes
686				for idxobj in idxobjs:
687					libnsX.netsnmp_table_dataset_add_index(
688						self._dataset,
689						idxobj._asntype
690					)
691
692				# Define the table's columns and their default values
693				for coldef in coldefs:
694					colno    = coldef[0]
695					defobj   = coldef[1]
696					writable = coldef[2] if len(coldef) > 2 \
697					                     else 0
698
699					result = libnsX.netsnmp_table_set_add_default_row(
700						self._dataset,
701						colno,
702						defobj._asntype,
703						writable,
704						defobj.cref(),
705						defobj._data_size
706					)
707					if result != SNMPERR_SUCCESS:
708						raise netsnmpAgentException(
709							"netsnmp_table_set_add_default_row() failed with "
710							"error code {0}!".format(result)
711						)
712
713				# Register handler and table_data_set with net-snmp.
714				self._handler_reginfo = agent._prepareRegistration(
715					oidstr,
716					extendable
717				)
718				self._handler_reginfo.contents.contextName = b(context)
719				result = libnsX.netsnmp_register_table_data_set(
720					self._handler_reginfo,
721					self._dataset,
722					None
723				)
724				if result != SNMP_ERR_NOERROR:
725					raise netsnmpAgentException(
726						"Error code {0} while registering table with "
727						"net-snmp!".format(result)
728					)
729
730				# Finally, we keep track of all registered SNMP objects for the
731				# getRegistered() method.
732				agent._objs[context][oidstr] = self
733
734				# If "counterobj" was specified, use it to track the number
735				# of table rows
736				if counterobj:
737					counterobj.update(0)
738				self._counterobj = counterobj
739
740			def addRow(self, idxobjs):
741				dataset = self._dataset
742
743				# Define a Python class to provide access to the table row.
744				class TableRow(object):
745					def __init__(self, idxobjs):
746						# Create the netsnmp_table_set_storage structure for
747						# this row.
748						self._table_row = libnsX.netsnmp_table_data_set_create_row_from_defaults(
749							dataset.contents.default_row
750						)
751
752						# Add the indexes
753						for idxobj in idxobjs:
754							result = libnsa.snmp_varlist_add_variable(
755								ctypes.pointer(self._table_row.contents.indexes),
756								None,
757								0,
758								idxobj._asntype,
759								idxobj.cref(is_table_index=True),
760								idxobj._data_size
761							)
762							if result == None:
763								raise netsnmpAgentException("snmp_varlist_add_variable() failed!")
764
765					def setRowCell(self, column, snmpobj):
766						result = libnsX.netsnmp_set_row_column(
767							self._table_row,
768							column,
769							snmpobj._asntype,
770							snmpobj.cref(),
771							snmpobj._data_size
772						)
773						if result != SNMPERR_SUCCESS:
774							raise netsnmpAgentException("netsnmp_set_row_column() failed with error code {0}!".format(result))
775
776				row = TableRow(idxobjs)
777
778				libnsX.netsnmp_table_dataset_add_row(
779					dataset,        # *table
780					row._table_row  # row
781				)
782
783				if self._counterobj:
784					self._counterobj.update(self._counterobj.value() + 1)
785
786				return row
787
788			def value(self):
789				# Because tables are more complex than scalar variables, we
790				# return a dictionary representing the table's structure and
791				# contents instead of a simple string.
792				retdict = {}
793
794				# The first entry will contain the defined columns, their types
795				# and their defaults, if set. We use array index 0 since it's
796				# impossible for SNMP tables to have a row with that index.
797				retdict[0] = {}
798				col = self._dataset.contents.default_row
799				while bool(col):
800					retdict[0][int(col.contents.column)] = {}
801
802					asntypes = {
803						ASN_INTEGER:    "Integer",
804						ASN_OCTET_STR:  "OctetString",
805						ASN_IPADDRESS:  "IPAddress",
806						ASN_COUNTER:    "Counter32",
807						ASN_COUNTER64:  "Counter64",
808						ASN_UNSIGNED:   "Unsigned32",
809						ASN_TIMETICKS:  "TimeTicks"
810					}
811					retdict[0][int(col.contents.column)]["type"] = asntypes[col.contents.type]
812					if bool(col.contents.data):
813						if col.contents.type == ASN_OCTET_STR:
814							retdict[0][int(col.contents.column)]["value"] = u(ctypes.string_at(col.contents.data.string, col.contents.data_len))
815						elif col.contents.type == ASN_IPADDRESS:
816							uint_value = ctypes.cast(
817								(ctypes.c_int*1)(col.contents.data.integer.contents.value),
818								ctypes.POINTER(ctypes.c_uint)
819							).contents.value
820							retdict[0][int(col.contents.column)]["value"] = socket.inet_ntoa(struct.pack("I", uint_value))
821						else:
822							retdict[0][int(col.contents.column)]["value"] = col.contents.data.integer.contents.value
823					col = col.contents.next
824
825				# Next we iterate over the table's rows, creating a dictionary
826				# entry for each row after that row's index.
827				row = self._dataset.contents.table.contents.first_row
828				while bool(row):
829					# We want to return the row index in the same way it is
830					# shown when using "snmptable", eg. "aa" instead of 2.97.97.
831					# This conversion is actually quite complicated (see
832					# net-snmp's sprint_realloc_objid() in snmplib/mib.c and
833					# get*_table_entries() in apps/snmptable.c for details).
834					# All code below assumes eg. that the OID output format was
835					# not changed.
836
837					# snprint_objid() below requires a _full_ OID whereas the
838					# table row contains only the current row's identifer.
839					# Unfortunately, net-snmp does not have a ready function to
840					# get the full OID. The following code was modelled after
841					# similar code in netsnmp_table_data_build_result().
842					fulloid = ctypes.cast(
843						ctypes.create_string_buffer(
844							MAX_OID_LEN * ctypes.sizeof(c_oid)
845						),
846						c_oid_p
847					)
848
849					# Registered OID
850					rootoidlen = self._handler_reginfo.contents.rootoid_len
851					for i in range(0, rootoidlen):
852						fulloid[i] = self._handler_reginfo.contents.rootoid[i]
853
854					# Entry
855					fulloid[rootoidlen] = 1
856
857					# Fake the column number. Unlike the table_data and
858					# table_data_set handlers, we do not have one here. No
859					# biggie, using a fixed value will do for our purposes as
860					# we'll do away with anything left of the first dot below.
861					fulloid[rootoidlen + 1] = 2
862
863					# Index data
864					indexoidlen = row.contents.index_oid_len
865					for i in range(0, indexoidlen):
866						fulloid[rootoidlen + 2 + i] = row.contents.index_oid[i]
867
868					# Convert the full OID to its string representation
869					oidcstr = ctypes.create_string_buffer(MAX_OID_LEN)
870					libnsa.snprint_objid(
871						oidcstr,
872						MAX_OID_LEN,
873						fulloid,
874						rootoidlen + 2 + indexoidlen
875					)
876
877					# And finally do away with anything left of the first dot
878					# so we keep the row index only
879					indices = oidcstr.value.split(b".", 1)[1]
880
881					# If it's a string, remove the double quotes. If it's a
882					# string containing an integer, make it one
883					try:
884						indices = int(indices)
885					except ValueError:
886						indices = u(indices.replace(b'"', b''))
887
888					# Finally, iterate over all columns for this row and add
889					# stored data, if present
890					retdict[indices] = {}
891					data = ctypes.cast(row.contents.data, ctypes.POINTER(netsnmp_table_data_set_storage))
892					while bool(data):
893						if bool(data.contents.data):
894							if data.contents.type == ASN_OCTET_STR:
895								retdict[indices][int(data.contents.column)] = u(ctypes.string_at(data.contents.data.string, data.contents.data_len))
896							elif data.contents.type == ASN_COUNTER64:
897								retdict[indices][int(data.contents.column)] = data.contents.data.counter64.contents.value
898							elif data.contents.type == ASN_IPADDRESS:
899								uint_value = ctypes.cast((ctypes.c_int*1)(
900									data.contents.data.integer.contents.value),
901									ctypes.POINTER(ctypes.c_uint)
902									).contents.value
903								retdict[indices][int(data.contents.column)] = socket.inet_ntoa(struct.pack("I", uint_value))
904							else:
905								retdict[indices][int(data.contents.column)] = data.contents.data.integer.contents.value
906						else:
907							retdict[indices] += {}
908						data = data.contents.next
909
910					row = row.contents.next
911
912				return retdict
913
914			def clear(self):
915				row = self._dataset.contents.table.contents.first_row
916				while bool(row):
917					nextrow = row.contents.next
918					libnsX.netsnmp_table_dataset_remove_and_delete_row(
919						self._dataset,
920						row
921					)
922					row = nextrow
923				if self._counterobj:
924					self._counterobj.update(0)
925
926		# Return an instance of the just-defined class to the agent
927		return Table(oidstr, indexes, columns, counterobj, extendable, context)
928
929	def getContexts(self):
930		""" Returns the defined contexts. """
931
932		return self._objs.keys()
933
934	def getRegistered(self, context = ""):
935		""" Returns a dictionary with the currently registered SNMP objects.
936
937		    Returned is a dictionary objects for the specified "context",
938		    which defaults to the default context. """
939		myobjs = {}
940		try:
941			# Python 2.x
942			objs_iterator = self._objs[context].iteritems()
943		except AttributeError:
944			# Python 3.x
945			objs_iterator = self._objs[context].items()
946		for oidstr, snmpobj in objs_iterator:
947			myobjs[oidstr] = {
948				"type": type(snmpobj).__name__,
949				"value": snmpobj.value()
950			}
951		return dict(myobjs)
952
953	def start(self):
954		""" Starts the agent. Among other things, this means connecting
955		    to the master agent, if configured that way. """
956		if  self._status != netsnmpAgentStatus.CONNECTED \
957		and self._status != netsnmpAgentStatus.RECONNECTING:
958			self._status = netsnmpAgentStatus.FIRSTCONNECT
959			libnsa.init_snmp(b(self.AgentName))
960			if self._status == netsnmpAgentStatus.CONNECTFAILED:
961				msg = "Error connecting to snmpd instance at \"{0}\" -- " \
962				      "incorrect \"MasterSocket\" or snmpd not running?"
963				msg = msg.format(self.MasterSocket)
964				raise netsnmpAgentException(msg)
965
966	def check_and_process(self, block=True):
967		""" Processes incoming SNMP requests.
968		    If optional "block" argument is True (default), the function
969		    will block until a SNMP packet is received. """
970		return libnsa.agent_check_and_process(int(bool(block)))
971
972	def shutdown(self):
973		libnsa.snmp_shutdown(b(self.AgentName))
974
975		# Unfortunately we can't safely call shutdown_agent() for the time
976		# being. All net-snmp versions up to and including 5.7.3 are unable
977		# to do proper cleanup and cause issues such as double free()s so that
978		# one effectively has to rely on the OS to release resources.
979		#libnsa.shutdown_agent()
980
981class netsnmpAgentException(Exception):
982	pass
983