fix(snmp): bundle pysnmp in image and port poller to the asyncio HLAPI
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 46s
CI / integration (push) Successful in 2m21s
CI / publish (push) Successful in 1m16s

The SNMP plugin ships in the image but logged "pysnmp not installed — SNMP
polling disabled" on every poll, so polling never worked. Two coupled defects:

1. The Dockerfile installed only `.[ansible]`, so the `snmp` extra (pysnmp)
   was never bundled even though the plugin is first-party and shipped.
2. poller.py used the synchronous pysnmp HLAPI (`next(getCmd(...))`), which
   pysnmp-lextudio 6.x removed — it's asyncio-only now — so even with the dep
   present, polling would have thrown and silently returned nothing. The 5.x
   line that still has the sync API isn't safe on the image's Python 3.13.

Fix:
- Dockerfile: install `.[ansible,snmp]`.
- poller.py: `poll_device_sync` → `async def poll_device` on the asyncio HLAPI,
  with a dual-version import (pysnmp 7.x `pysnmp.hlapi.v3arch.asyncio`/`get_cmd`
  + async `UdpTransportTarget.create`; pysnmp-lextudio 6.2.x
  `pysnmp.hlapi.asyncio`/`getCmd` + direct `UdpTransportTarget`) so a dependency
  bump can't silently re-break it.
- scheduler.py: await poll_device directly; drop the run_in_executor wrapper
  and the now-unused asyncio import.
- Add tests/plugins/snmp/test_poller.py covering the version→mpModel mapping,
  that the poller is a coroutine, and the graceful no-pysnmp path.

Note: CI confirms import/load and the no-pysnmp path, but has no SNMP target —
live polling against real devices is verified after deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
2026-06-19 13:28:07 -04:00
parent 88091936c5
commit 4fc8c96c41
5 changed files with 101 additions and 39 deletions
+2 -9
View File
@@ -1,6 +1,5 @@
# plugins/snmp/scheduler.py
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING
@@ -27,15 +26,13 @@ def make_poll_task(app: "Quart") -> ScheduledTask:
async def _do_poll(app: "Quart") -> None:
from .poller import poll_device_sync
from .poller import poll_device
from steward.core.alerts import record_metric
devices: list[dict] = app.config["PLUGINS"]["snmp"].get("devices", [])
if not devices:
return
loop = asyncio.get_event_loop()
async with app.db_sessionmaker() as session:
async with session.begin():
for device in devices:
@@ -52,11 +49,7 @@ async def _do_poll(app: "Quart") -> None:
continue
try:
readings = await loop.run_in_executor(
None,
poll_device_sync,
host, port, community, version, oids,
)
readings = await poll_device(host, port, community, version, oids)
except Exception:
logger.exception("SNMP poll failed for device %s (%s)", name, host)
continue