diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..bda0415 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,12 @@ +snmpclient is written and maintained by Dennis Kaarsemaker and +various contributors: + +Development Lead +```````````````` + +- Dennis Kaarsemaker + +Patches and Suggestions +``````````````````````` + +- Robert Wlodarczyk diff --git a/README.rst b/README.rst index 202d73b..a57d14c 100644 --- a/README.rst +++ b/README.rst @@ -34,10 +34,11 @@ The SnmpClient class This class wraps arround pysnmp's cmdgen.CommandGenerator to make it easier to address an snmp daemon. -snmpclient.SnmpClient(host, port, authorizations) ----------------------------------------- -The constructor takes a hostname/ip address, UDP port and a list of -authorization objects. +snmpclient.SnmpClient(host, port, read_authorizations, write_authorizations, timeout, retries) +---------------------------------------------------------------------------------------------- +The constructor takes a hostname/ip address, UDP port, and a list of +read authorization objects. Optionally, a list of write authorization objects, +timeout, and number of retries may be provided as well. These objects are created as follows: @@ -60,6 +61,11 @@ snmpclient.SnmpClient.get(oid) Takes a named oid, queries the server and returns the value of that oid on the server. +snmpclient.SnmpClient.set(oid, value) +------------------------------ +Takes a named oid and value, queries the server to determine its type, and +subsequently sets value to the newly provided one. + snmpclient.SnmpClient.gettable(oid) ----------------------------------- Takes a named oid, walks that table and returns a list of (oid, value) pairs in diff --git a/setup.py b/setup.py index 2eb65e6..b8704e7 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ "Topic :: System :: Monitoring", "Topic :: Software Development" ], - #install_requires='pysnmp', + install_requires=['pysnmp'], ) diff --git a/snmpclient.py b/snmpclient.py index 6ee5ae4..66eacab 100644 --- a/snmpclient.py +++ b/snmpclient.py @@ -19,11 +19,12 @@ import pysnmp.entity.rfc3413.oneliner.cmdgen as cmdgen from pysnmp.smi import builder, view from pysnmp.smi.error import SmiError +from pysnmp.proto import rfc1902 __all__ = ['V1', 'V2', 'V2C', 'add_mib_path', 'load_mibs', 'nodeinfo', 'nodename', 'nodeid', 'SnmpClient', 'cmdgen'] -# Snmp version constants +# SNMP version constants V1 = 0 V2 = V2C = 1 @@ -31,9 +32,12 @@ __mibBuilder = builder.MibBuilder() __mibViewController = view.MibViewController(__mibBuilder) + def add_mib_path(*path): """Add a directory to the MIB search path""" - __mibBuilder.setMibPath(*(__mibBuilder.getMibPath() + path)) + mibPath = __mibBuilder.getMibPath() + path + __mibBuilder.setMibPath(*mibPath) + def load_mibs(*modules): """Load one or more mibs""" @@ -45,16 +49,15 @@ def load_mibs(*modules): continue raise -# Load basic mibs that come with pysnmp -load_mibs('SNMPv2-MIB','IF-MIB','IP-MIB','HOST-RESOURCES-MIB','FIBRE-CHANNEL-FE-MIB') def nodeinfo(oid): """Translate dotted-decimal oid to a tuple with symbolic info""" if isinstance(oid, basestring): oid = tuple([int(x) for x in oid.split('.') if x]) - return (__mibViewController.getNodeLocation(oid), + return (__mibViewController.getNodeLocation(oid), __mibViewController.getNodeName(oid)) + def nodename(oid): """Translate dotted-decimal oid or oid tuple to symbolic name""" oid = __mibViewController.getNodeLocation(oid) @@ -63,7 +66,8 @@ def nodename(oid): if noid: name += '.' + noid return name - + + def nodeid(oid): """Translate named oid to dotted-decimal format""" ids = oid.split('.') @@ -73,41 +77,122 @@ def nodeid(oid): oid = mibnode.getName() + ids return oid + +# Load basic mibs that come with pysnmp +load_mibs('SNMPv2-MIB', + 'IF-MIB', + 'IP-MIB', + 'HOST-RESOURCES-MIB', + 'FIBRE-CHANNEL-FE-MIB') + + class SnmpClient(object): """Easy access to an snmp deamon on a host""" - def __init__(self, host, port, authorizations): + def __init__(self, host, port, read_authorizations, write_authorizations, + timeout=1, retries=2): """Set up the client and detect the community to use""" self.host = host self.port = port + self.timeout = timeout + self.retries = retries self.alive = False + self.transport = cmdgen.UdpTransportTarget((self.host, self.port), + timeout=self.timeout, + retries=self.retries,) + self.readauth = None + self.writeauth = None - # Which community to use + # Determine which community to use for reading values noid = nodeid('SNMPv2-MIB::sysName.0') - for auth in authorizations: + for auth in read_authorizations: (errorIndication, errorStatus, errorIndex, varBinds) = \ - cmdgen.CommandGenerator().getCmd(auth, cmdgen.UdpTransportTarget((self.host, self.port)), noid) + cmdgen.CommandGenerator().getCmd( + cmdgen.CommunityData(auth['community'], + mpModel=auth['version']), + self.transport, + noid) if errorIndication == 'requestTimedOut': continue else: self.alive = True - self.auth = auth + self.readauth = cmdgen.CommunityData(auth['community'], + mpModel=auth['version']) break + # Don't determine the write authorization since there's no temporary + # location within SNMP to write to. Choose the first authorization. + for auth in write_authorizations: + self.writeauth = cmdgen.CommunityData(auth['community'], + mpModel=auth['version']) + break + def get(self, oid): """Get a specific node in the tree""" + if not self.readauth: + return + noid = nodeid(oid) (errorIndication, errorStatus, errorIndex, varBinds) = \ - cmdgen.CommandGenerator().getCmd(self.auth, cmdgen.UdpTransportTarget((self.host, self.port)), noid) + cmdgen.CommandGenerator().getCmd( + self.readauth, + self.transport, + noid) if errorIndication: raise RuntimeError("SNMPget of %s on %s failed" % (oid, self.host)) return varBinds[0][1] + def set(self, oid, value): + """Set a specific value to a node in the tree""" + if not self.writeauth: + return + + initial_value = self.get(oid) + + # Types from RFC-1902 + if isinstance(initial_value, rfc1902.Counter32): + set_value = rfc1902.Counter32(str(value)) + elif isinstance(initial_value, rfc1902.Counter64): + set_value = rfc1902.Counter64(str(value)) + elif isinstance(initial_value, rfc1902.Gauge32): + set_value = rfc1902.Gauge32(str(value)) + elif isinstance(initial_value, rfc1902.Integer): + set_value = rfc1902.Integer(str(value)) + elif isinstance(initial_value, rfc1902.Integer32): + set_value = rfc1902.Integer32(str(value)) + elif isinstance(initial_value, rfc1902.IpAddress): + set_value = rfc1902.IpAddress(str(value)) + elif isinstance(initial_value, rfc1902.OctetString): + set_value = rfc1902.OctetString(str(value)) + elif isinstance(initial_value, rfc1902.TimeTicks): + set_value = rfc1902.TimeTicks(str(value)) + elif isinstance(initial_value, rfc1902.Unsigned32): + set_value = rfc1902.Unsigned32(str(value)) + else: + raise RuntimeError("Unknown type %s" % type(initial_value)) + + noid = nodeid(oid) + (errorIndication, errorStatus, errorIndex, varBinds) = \ + cmdgen.CommandGenerator().setCmd( + self.writeauth, + self.transport, + (noid, set_value) + ) + if errorIndication: + raise RuntimeError("SNMPset of %s on %s failed" % (oid, self.host)) + return varBinds[0][1] + def gettable(self, oid): """Get a complete subtable""" + if not self.readauth: + return + noid = nodeid(oid) (errorIndication, errorStatus, errorIndex, varBinds) = \ - cmdgen.CommandGenerator().nextCmd(self.auth, cmdgen.UdpTransportTarget((self.host, self.port)), noid) + cmdgen.CommandGenerator().nextCmd( + self.readauth, + self.transport, + noid) if errorIndication: raise RuntimeError("SNMPget of %s on %s failed" % (oid, self.host)) return [x[0] for x in varBinds] @@ -115,6 +200,9 @@ def gettable(self, oid): def matchtables(self, index, tables): """Match a list of tables using either a specific index table or the common tail of the OIDs in the tables""" + if not self.readauth: + return + oid_to_index = {} result = {} indexlen = 1 diff --git a/test_snmpclient.py b/test_snmpclient.py new file mode 100644 index 0000000..d45d780 --- /dev/null +++ b/test_snmpclient.py @@ -0,0 +1,15 @@ +import snmpclient + +router = '10.100.184.62' +port = 161 +public_auth = [{'community': 'public', 'version': snmpclient.V2C}] +private_auth = [{'community': 'private', 'version': snmpclient.V2C}] + +client = snmpclient.SnmpClient(router, port, public_auth, private_auth) +print client.alive +print client.get('SNMPv2-MIB::sysName.0') +print client.gettable('UDP-MIB::udpLocalAddress') +print client.matchtables('IF-MIB::ifIndex', + ('IF-MIB::ifDescr', + 'IF-MIB::ifPhysAddress', + 'IF-MIB::ifOperStatus')) diff --git a/test_snmpclient_apcpdu.py b/test_snmpclient_apcpdu.py new file mode 100644 index 0000000..16c9ac9 --- /dev/null +++ b/test_snmpclient_apcpdu.py @@ -0,0 +1,38 @@ +from os import getcwd +import snmpclient + +router = '10.100.184.62' +port = 161 +public_auth = [{'community': 'public', 'version': snmpclient.V2C}] +private_auth = [{'community': 'private', 'version': snmpclient.V2C}] + +snmpclient.add_mib_path(getcwd()) + +# NOTE: In order to use this, download the APC PowerNet-MIB from: +# ftp://ftp.apc.com/apc/public/software/pnetmib/mib +# and run: +# build-pysnmp-mib -o PowerNet-MIB.py powernetZZZ.mib +# There appears to be a bug in either the MIB or build-pysnmp-mib whereby +# Unsigned32 is undefined. Despite this being an awful hack, modify the +# generated PowerNet-MIB.py running: +# sed -i '10 a ( Unsigned32, ) = mibBuilder.importSymbols("SNMPv2-SMI", "Unsigned32")' PowerNet-MIB.py +snmpclient.load_mibs('PowerNet-MIB') +client = snmpclient.SnmpClient(router, port, public_auth, private_auth) + +print client.alive +print "APC Model Number: %s" % \ + client.get('PowerNet-MIB::rPDU2IdentModelNumber.1') +print "Outlet 5 status: %s" % \ + client.get('PowerNet-MIB::rPDU2OutletSwitchedStatusState.5') +print "Outlet 5 on: %s" % \ + client.set('PowerNet-MIB::rPDU2OutletSwitchedControlCommand.5', 1) +print "Outlet 5 command pending: %s" % \ + client.get('PowerNet-MIB::rPDU2OutletSwitchedStatusCommandPending.5') +print "Outlet 5 status: %s" % \ + client.get('PowerNet-MIB::rPDU2OutletSwitchedStatusState.5') +print "Outlet 5 off: %s" % \ + client.set('PowerNet-MIB::rPDU2OutletSwitchedControlCommand.5', 1) +print "Outlet 5 command pending: %s" % \ + client.get('PowerNet-MIB::rPDU2OutletSwitchedStatusCommandPending.5') +print "Outlet 5 status: %s" % \ + client.get('PowerNet-MIB::rPDU2OutletSwitchedStatusState.5') diff --git a/test_snmplient.py b/test_snmplient.py deleted file mode 100644 index 85c90ca..0000000 --- a/test_snmplient.py +++ /dev/null @@ -1,10 +0,0 @@ -import snmpclient - -router = '10.42.1.1' -authdata = {'community': 'public', 'version': snmpclient.V2C} - -client = snmpclient.SnmpClient(router, [authdata]) -print client.alive -print client.get('SNMPv2-MIB::sysName.0') -print client.gettable('UDP-MIB::udpLocalAddress') -print client.matchtables('IF-MIB::ifIndex', ('IF-MIB::ifDescr', 'IF-MIB::ifPhysAddress', 'IF-MIB::ifOperStatus'))