153 lines
4.7 KiB
Python
153 lines
4.7 KiB
Python
# sensgw/protocols/snmp.py
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from dataclasses import dataclass
|
|
|
|
from ..models import Endpoint, Device, Channel
|
|
from ..writer import Writer
|
|
from .polling import poll_forever
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SnmpBinding:
|
|
endpoint: Endpoint
|
|
device: Device
|
|
channel: Channel
|
|
oid: str
|
|
datatype: str # "float" | "int" | ...
|
|
|
|
|
|
def _parse_numeric(datatype: str, raw: str) -> float:
|
|
kind = (datatype or "float").strip().lower()
|
|
if kind == "int":
|
|
return float(int(raw))
|
|
return float(raw)
|
|
|
|
|
|
def _parse_version(conn: dict) -> int:
|
|
"""
|
|
Return mpModel:
|
|
SNMPv1 -> 0
|
|
SNMPv2c -> 1
|
|
"""
|
|
v = str(conn.get("version", "2c")).lower()
|
|
if v in {"1", "v1", "snmpv1"}:
|
|
return 0
|
|
return 1
|
|
|
|
|
|
class SnmpEndpointCollector:
|
|
def __init__(self, writer: Writer, default_poll_s: int):
|
|
self.writer = writer
|
|
self.default_poll_s = default_poll_s
|
|
|
|
async def _get_many(
|
|
self,
|
|
*,
|
|
host: str,
|
|
port: int,
|
|
community: str,
|
|
mp_model: int,
|
|
timeout_s: int,
|
|
oids: list[str],
|
|
) -> dict[str, str]:
|
|
from pysnmp.hlapi.v3arch.asyncio import ( # type: ignore
|
|
SnmpEngine,
|
|
CommunityData,
|
|
UdpTransportTarget,
|
|
ContextData,
|
|
ObjectType,
|
|
ObjectIdentity,
|
|
get_cmd,
|
|
)
|
|
|
|
snmp_engine = SnmpEngine()
|
|
try:
|
|
var_binds = [ObjectType(ObjectIdentity(oid)) for oid in oids]
|
|
|
|
# In pysnmp 7.x, target creation is async:
|
|
target = await UdpTransportTarget.create((host, port), timeout=timeout_s, retries=0)
|
|
|
|
iterator = get_cmd(
|
|
snmp_engine,
|
|
CommunityData(community, mpModel=mp_model),
|
|
target,
|
|
ContextData(),
|
|
*var_binds,
|
|
)
|
|
|
|
error_indication, error_status, error_index, out_binds = await iterator
|
|
|
|
if error_indication:
|
|
raise RuntimeError(str(error_indication))
|
|
if error_status:
|
|
raise RuntimeError(
|
|
f"{error_status.prettyPrint()} at "
|
|
f"{out_binds[int(error_index) - 1][0] if error_index else '?'}"
|
|
)
|
|
|
|
return {str(name): str(val) for name, val in out_binds}
|
|
finally:
|
|
snmp_engine.close_dispatcher()
|
|
|
|
async def run_endpoint(self, endpoint: Endpoint, bindings: list[SnmpBinding]) -> None:
|
|
host = endpoint.conn["host"]
|
|
port = int(endpoint.conn.get("port", 161))
|
|
community = endpoint.conn.get("community", "public")
|
|
timeout_s = int(endpoint.conn.get("timeout_s", 2))
|
|
mp_model = _parse_version(endpoint.conn)
|
|
|
|
intervals = [
|
|
int(b.channel.poll_interval_s)
|
|
for b in bindings
|
|
if b.channel.poll_interval_s is not None
|
|
]
|
|
interval_s = min(intervals) if intervals else self.default_poll_s
|
|
|
|
oid_to_binding: dict[str, SnmpBinding] = {b.oid.strip(): b for b in bindings}
|
|
oids = list(oid_to_binding.keys())
|
|
|
|
async def read_once() -> None:
|
|
ts = dt.datetime.now(dt.timezone.utc)
|
|
try:
|
|
values = await self._get_many(
|
|
host=host,
|
|
port=port,
|
|
community=community,
|
|
mp_model=mp_model,
|
|
timeout_s=timeout_s,
|
|
oids=oids,
|
|
)
|
|
|
|
for oid_str, raw in values.items():
|
|
b = oid_to_binding.get(oid_str)
|
|
if b is None:
|
|
continue
|
|
try:
|
|
v = _parse_numeric(b.datatype, raw)
|
|
v = v * b.channel.scale_value + b.channel.offset_value
|
|
|
|
await self.writer.write_metric(
|
|
ts=ts,
|
|
device_id=b.device.device_id,
|
|
location_id=b.device.location_id,
|
|
metric=b.channel.metric,
|
|
value=v,
|
|
)
|
|
except Exception as e:
|
|
await self.writer.write_error(
|
|
device_id=b.device.device_id,
|
|
error=f"snmp parse/write: {e}",
|
|
)
|
|
except Exception as e:
|
|
# Endpoint-level failure: mark all devices as error
|
|
for b in bindings:
|
|
await self.writer.write_error(
|
|
device_id=b.device.device_id,
|
|
error=f"snmp endpoint: {e}",
|
|
)
|
|
|
|
await poll_forever(interval_s=interval_s, read_once=read_once)
|
|
|