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