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