Serving values the agent has to wait for¶
Serving values the agent has to wait for¶
Listen and respond to SNMP GET/GETNEXT/GETBULK/SET queries with the following options:
SNMPv2c, community ‘public’
serving objects from an instrumentation controller whose operations are coroutines, standing in for a database or a remote service
allow access to objects under 1.3.6.1.4.1.20408
over IPv4/UDP, listening at 127.0.0.1:1161
The controller waits before it can answer, and the engine goes on serving other requests while it does – so one slow object does not stall the agent.
Either of the following Net-SNMP commands will exercise this Agent:
$ snmpget -v2c -c public 127.0.0.1:1161 1.3.6.1.4.1.20408.999.1.0
$ snmpwalk -v2c -c public 127.0.0.1:1161 1.3.6.1.4.1.20408
$ snmpbulkwalk -v2c -c public 127.0.0.1:1161 1.3.6.1.4.1.20408
import asyncio
from pysnmp.carrier.asyncio.dgram import udp
from pysnmp.entity import config, engine
from pysnmp.entity.rfc3413 import cmdrsp, context
from pysnmp.proto import rfc1902
from pysnmp.smi import exval
from pysnmp.smi.error import NotWritableError
# The objects this agent serves, in the lexicographic order a walk needs.
OBJECTS = {
(1, 3, 6, 1, 4, 1, 20408, 999, 1, 0): "the first value",
(1, 3, 6, 1, 4, 1, 20408, 999, 2, 0): "the second value",
(1, 3, 6, 1, 4, 1, 20408, 999, 3, 0): "the third value",
}
class SlowInstrum:
"""An instrumentation controller that has to wait for its values.
The three operations are the whole contract; nothing has to be inherited.
Declaring them `async def` is how a controller says it cannot answer
without waiting, which is what an agent reading from a database, a REST
service or another device is really doing.
"""
async def _fetch(self, oid):
"""Stands in for the query that has to go somewhere and come back."""
await asyncio.sleep(0.25)
return rfc1902.OctetString(OBJECTS[oid])
async def readVars(self, varBinds, **context):
"""Read exactly the objects named."""
result = []
for oid, _ in varBinds:
name = tuple(oid)
if name in OBJECTS:
result.append((oid, await self._fetch(name)))
else:
result.append((oid, exval.noSuchInstance))
return result
async def readNextVars(self, varBinds, **context):
"""Read the objects following those named, which is what a walk asks for."""
ordered = sorted(OBJECTS)
result = []
for oid, _ in varBinds:
following = next((o for o in ordered if o > tuple(oid)), None)
if following is None:
result.append((oid, exval.endOfMibView))
else:
result.append(
(rfc1902.ObjectName(following), await self._fetch(following))
)
return result
async def writeVars(self, varBinds, **context):
"""Refuse every write. Reads are what this agent is for.
Refusing is raising. Bindings handed back are what the agent agrees it
wrote, so returning `noSuchInstance` values would answer with a SET
that succeeded and carried them.
"""
raise NotWritableError(idx=0, name=varBinds[0][0])
async def main():
"""Serve requests until interrupted."""
snmpEngine = engine.SnmpEngine()
config.addTransport(
snmpEngine,
udp.domainName,
udp.UdpTransport().openServerMode(("127.0.0.1", 1161)),
)
config.addV1System(snmpEngine, "my-area", "public")
config.addVacmUser(
snmpEngine, 2, "my-area", "noAuthNoPriv", (1, 3, 6, 1, 4, 1, 20408)
)
# A controller is whatever object is registered for the context, so this
# one replaces the MIB-backed controller the context would have built.
snmpContext = context.SnmpContext(snmpEngine)
snmpContext.contextNames[b""] = SlowInstrum()
cmdrsp.GetCommandResponder(snmpEngine, snmpContext)
cmdrsp.NextCommandResponder(snmpEngine, snmpContext)
cmdrsp.BulkCommandResponder(snmpEngine, snmpContext)
cmdrsp.SetCommandResponder(snmpEngine, snmpContext)
try:
await asyncio.Event().wait()
finally:
await snmpEngine.transportDispatcher.closeDispatcherAsync()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Download script.
See also: serving values an agent has to wait for.