af60ca446d
The fabledscryer->steward rename had only ever reached host_agent. The other five bundled plugins (http, snmp, traefik, unifi, docker) still imported `from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env vars — so every one of them was broken at import since the original rebrand. CI stayed green only because none are enabled by default and migrations don't import plugin modules. Now that they version in-tree, complete the rename: - fabledscryer.* -> steward.* imports across all five plugins - FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files - author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward - snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever reappear in shipped code (the drift bit twice; this stops a third time). Also clears pre-existing ruff lint debt (unused imports, semicolon statements, mid-file import) surfaced by the new lint lane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
2.7 KiB
Python
95 lines
2.7 KiB
Python
# plugins/snmp/poller.py
|
|
"""
|
|
Synchronous SNMP GET helper, run via executor.
|
|
|
|
Requires pysnmp-lextudio (maintained pysnmp fork):
|
|
pip install 'steward[snmp]'
|
|
|
|
If pysnmp is not installed, poll_device() returns an empty dict and logs a warning.
|
|
"""
|
|
from __future__ import annotations
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _pysnmp_available() -> bool:
|
|
try:
|
|
import pysnmp # noqa: F401
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
|
|
def _mp_model(version: str) -> int:
|
|
"""Map version string to pysnmp mpModel integer."""
|
|
return 0 if version == "1" else 1
|
|
|
|
|
|
def poll_device_sync(
|
|
host: str,
|
|
port: int,
|
|
community: str,
|
|
version: str,
|
|
oids: list[dict],
|
|
) -> dict[str, float]:
|
|
"""
|
|
Perform SNMP GET for each OID and return {label: float_value}.
|
|
Non-numeric OIDs (strings, etc.) are skipped.
|
|
Returns empty dict on any error.
|
|
"""
|
|
if not _pysnmp_available():
|
|
logger.warning("pysnmp not installed — SNMP polling disabled. "
|
|
"Install with: pip install 'steward[snmp]'")
|
|
return {}
|
|
|
|
from pysnmp.hlapi import (
|
|
CommunityData,
|
|
ContextData,
|
|
ObjectIdentity,
|
|
ObjectType,
|
|
SnmpEngine,
|
|
UdpTransportTarget,
|
|
getCmd,
|
|
)
|
|
|
|
results: dict[str, float] = {}
|
|
engine = SnmpEngine()
|
|
|
|
for oid_cfg in oids:
|
|
oid = oid_cfg["oid"]
|
|
label = oid_cfg.get("label") or oid
|
|
scale = float(oid_cfg.get("scale", 1.0))
|
|
|
|
try:
|
|
error_indication, error_status, error_index, var_binds = next(
|
|
getCmd(
|
|
engine,
|
|
CommunityData(community, mpModel=_mp_model(version)),
|
|
UdpTransportTarget((host, port), timeout=5, retries=1),
|
|
ContextData(),
|
|
ObjectType(ObjectIdentity(oid)),
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("SNMP GET %s@%s OID %s failed: %s", host, port, oid, exc)
|
|
continue
|
|
|
|
if error_indication:
|
|
logger.debug("SNMP error %s@%s OID %s: %s", host, port, oid, error_indication)
|
|
continue
|
|
if error_status:
|
|
logger.debug("SNMP status %s@%s OID %s: %s at %s",
|
|
host, port, oid, error_status.prettyPrint(),
|
|
error_index and var_binds[int(error_index) - 1][0] or "?")
|
|
continue
|
|
|
|
for _, val in var_binds:
|
|
try:
|
|
results[label] = float(val) * scale
|
|
except (TypeError, ValueError):
|
|
# Non-numeric type (e.g. OctetString description) — skip
|
|
logger.debug("SNMP non-numeric value for %s label=%s: %r", oid, label, val)
|
|
|
|
return results
|