"""
.. module:: snmp
:platform: Unix
:synopsis: A source module for polling SNMP. Requires PySNMP>=6.0
.. moduleauthor:: Colin Alston <colin@tamvera.com>
"""
import logging
from zope.interface import implementer
from pysnmp.hlapi.v3arch.asyncio import (
SnmpEngine, CommunityData, UdpTransportTarget, ContextData,
ObjectType, ObjectIdentity, walk_cmd,
)
from pysnmp.proto import rfc1902
from duct.interfaces import IDuctSource
from duct.objects import Source
from duct import aggregators
log = logging.getLogger(__name__)
[docs]
class SNMPConnection(object):
"""A wrapper class for PySNMP asyncio functions.
:param host: SNMP agent host
:param port: SNMP port
:param community: SNMP read community
"""
def __init__(self, host, port, community):
self.host = host
self.port = port
self.community = community
self.engine = SnmpEngine()
async def _do_walk(self, obj_type, mp_model=0):
"""Shared walk implementation. Returns a list of (oid, value) tuples."""
results = []
target = await UdpTransportTarget.create(
(self.host, self.port), timeout=5, retries=2
)
async for (err_indication, err_status, err_index,
var_bind_table) in walk_cmd(
self.engine,
CommunityData(self.community, mpModel=mp_model),
target,
ContextData(),
obj_type,
lexicographicMode=False,
):
if err_indication:
log.warning('SNMP error: %s', err_indication)
break
if err_status:
idx = err_index and var_bind_table[-1][int(err_index) - 1][0]
log.warning('SNMP error status: %s at %s',
err_status.prettyPrint(), idx or '?')
break
for var_bind in var_bind_table:
results.append((var_bind[0], var_bind[1]))
return results
[docs]
async def walk(self, oid):
"""Walk OIDs under `oid`. Returns a list of (oid, value) tuples."""
return await self._do_walk(ObjectType(ObjectIdentity(oid)), mp_model=0)
[docs]
async def walkMib(self, mib_name, obj_name, mib_dirs=None):
"""Walk a MIB table by symbolic name. Returns a list of (oid, value) tuples.
:param mib_name: MIB module name (e.g. 'PDU2-MIB')
:param obj_name: Object name within the MIB (e.g. 'measurementsOutletSensorValue')
:param mib_dirs: List of directories containing compiled MIB modules
"""
identity = ObjectIdentity(mib_name, obj_name)
for d in (mib_dirs or []):
identity.addMibSource(d)
return await self._do_walk(ObjectType(identity), mp_model=1)
[docs]
@implementer(IDuctSource)
class SNMP(Source):
"""Connects to an SNMP agent and retrieves OIDs.
:param ip: SNMP agent host (default: 127.0.0.1)
:type ip: str.
:param port: SNMP port (default: 161)
:type port: int.
:param community: SNMP read community
:type community: str.
"""
def __init__(self, *a, **kw):
Source.__init__(self, *a, **kw)
host = self.config.get('ip', '127.0.0.1')
port = int(self.config.get('port', 161))
community = self.config.get('community', 'public')
self.snmp = SNMPConnection(host, port, community)
[docs]
async def getCounter(self, soid):
"""Walk SNMP OID and return list of (oid, value) tuples."""
return await self.snmp.walk(soid)
[docs]
async def getIfMetrics(self):
"""Retrieve interface metrics via SNMP and return Event list."""
ifaces = await self.snmp.walk('1.3.6.1.2.1.2.2.1.2')
table = [
('1.3.6.1.2.1.2.2.1.10', 'ifInOctets'),
('1.3.6.1.2.1.2.2.1.11', 'ifInUcastPkts'),
('1.3.6.1.2.1.2.2.1.12', 'ifInNUcastPkts'),
('1.3.6.1.2.1.2.2.1.14', 'ifInErrors'),
('1.3.6.1.2.1.2.2.1.13', 'ifInDiscards'),
('1.3.6.1.2.1.2.2.1.16', 'ifOutOctets'),
('1.3.6.1.2.1.2.2.1.17', 'ifOutUcastPkts'),
('1.3.6.1.2.1.2.2.1.18', 'ifOutNUcastPkts'),
('1.3.6.1.2.1.2.2.1.20', 'ifOutErrors'),
('1.3.6.1.2.1.2.2.1.19', 'ifOutDiscards'),
]
data = {}
for oid, key in table:
d = await self.getCounter(oid)
for i, (_, val) in enumerate(d):
if val and i < len(ifaces):
iface = str(ifaces[i][1])
if iface not in data:
data[iface] = {}
data[iface][key] = val
events = []
for iface, metrics in data.items():
for key, val in metrics.items():
aggr = None
if isinstance(val, rfc1902.Counter32):
aggr = aggregators.Counter32
if isinstance(val, rfc1902.Counter64):
aggr = aggregators.Counter64
events.append(self.createEvent(
'ok',
f'SNMP interface {iface} {key}={int(val):0.2f}',
int(val),
prefix=f'{iface}.{key}',
aggregation=aggr,
))
return events
[docs]
async def get(self):
return await self.getIfMetrics()
[docs]
class SNMPCisco837(SNMP):
"""Connects to a Cisco 837 and makes metrics.
:param ip: SNMP agent host (default: 127.0.0.1)
:param port: SNMP port (default: 161)
:param community: SNMP read community
"""
[docs]
async def get(self):
events = await self.getIfMetrics()
sync_us = (await self.snmp.walk('1.3.6.1.2.1.10.94.1.1.5'))[0][1]
sync_ds = (await self.snmp.walk('1.3.6.1.2.1.10.94.1.1.4'))[0][1]
sync_us = int(sync_us)
sync_ds = int(sync_ds)
events.append(self.createEvent(
'ok', f'SNMP ADSL sync downstream {sync_ds}',
sync_ds, prefix='adsl.rxrate'))
events.append(self.createEvent(
'ok', f'SNMP ADSL sync upstream {sync_us}',
sync_us, prefix='adsl.txrate'))
link = await self.snmp.walk('1.3.6.1.2.1.10.94.1.1.3')
link = {str(i): j for i, j in link}
output = int(link['1.3.6.1.2.1.10.94.1.1.3.1.7.15']) / 10.0
attn = int(link['1.3.6.1.2.1.10.94.1.1.3.1.5.15']) / 10.0
margin = int(link['1.3.6.1.2.1.10.94.1.1.3.1.4.15']) / 10.0
events.append(self.createEvent(
'ok', f'SNMP ADSL output power {output:0.2f}dBm',
output, prefix='adsl.outpwr'))
events.append(self.createEvent(
'ok', f'SNMP ADSL attenuation {attn:0.2f}dB',
attn, prefix='adsl.attn'))
events.append(self.createEvent(
'ok', f'SNMP ADSL noise margin {margin:0.2f}dB',
margin, prefix='adsl.margin'))
return events