Files
the-other-dude/backend/app/services/snmp_proxy.py
Jason Staack 655f1eadae feat(20-02): add parse-mib and test-profile API endpoints
- POST /snmp-profiles/parse-mib: upload MIB file, subprocess-call tod-mib-parser, return OID tree JSON
- POST /snmp-profiles/{id}/test: test profile connectivity via NATS discovery probe to poller
- New snmp_proxy service module following routeros_proxy.py lazy NATS pattern
- Pydantic schemas: MIBParseResponse, ProfileTestRequest, ProfileTestResponse, ProfileTestOIDResult
- MIB_PARSER_PATH config setting with /app/tod-mib-parser default
- MIB parse errors return 422, not 500; temp file cleanup in finally block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 20:21:08 -05:00

113 lines
3.5 KiB
Python

"""SNMP proxy via NATS request-reply.
Sends SNMP discovery/test requests to the Go poller's DiscoveryResponder
subscription (device.discover.snmp) and returns structured response data.
Used by:
- SNMP profile test endpoint (verify device connectivity with credentials)
"""
import json
import logging
from typing import Any, Optional
import nats
import nats.aio.client
from app.config import settings
logger = logging.getLogger(__name__)
# Module-level NATS connection (lazy initialized)
_nc: nats.aio.client.Client | None = None
async def _get_nats() -> nats.aio.client.Client:
"""Get or create a NATS connection for SNMP proxy requests."""
global _nc
if _nc is None or _nc.is_closed:
_nc = await nats.connect(settings.NATS_URL)
logger.info("SNMP proxy NATS connection established")
return _nc
async def snmp_discover(
ip_address: str,
snmp_port: int = 161,
snmp_version: str = "v2c",
community: Optional[str] = None,
security_level: Optional[str] = None,
username: Optional[str] = None,
auth_protocol: Optional[str] = None,
auth_passphrase: Optional[str] = None,
priv_protocol: Optional[str] = None,
priv_passphrase: Optional[str] = None,
) -> dict[str, Any]:
"""Send an SNMP discovery probe to a device via the Go poller.
Builds a DiscoveryRequest payload matching the Go DiscoveryRequest struct
and sends it to the poller's DiscoveryResponder via NATS request-reply.
Args:
ip_address: Target device IP address.
snmp_port: SNMP port (default 161).
snmp_version: "v1", "v2c", or "v3".
community: Community string for v1/v2c.
security_level: SNMPv3 security level.
username: SNMPv3 username.
auth_protocol: SNMPv3 auth protocol (e.g. "SHA").
auth_passphrase: SNMPv3 auth passphrase.
priv_protocol: SNMPv3 privacy protocol (e.g. "AES").
priv_passphrase: SNMPv3 privacy passphrase.
Returns:
{"sys_object_id": str, "sys_descr": str, "sys_name": str, "error": str|None}
"""
nc = await _get_nats()
payload: dict[str, Any] = {
"ip_address": ip_address,
"snmp_port": snmp_port,
"snmp_version": snmp_version,
}
# v1/v2c credentials
if community is not None:
payload["community"] = community
# v3 credentials
if security_level is not None:
payload["security_level"] = security_level
if username is not None:
payload["username"] = username
if auth_protocol is not None:
payload["auth_protocol"] = auth_protocol
if auth_passphrase is not None:
payload["auth_passphrase"] = auth_passphrase
if priv_protocol is not None:
payload["priv_protocol"] = priv_protocol
if priv_passphrase is not None:
payload["priv_passphrase"] = priv_passphrase
try:
reply = await nc.request(
"device.discover.snmp",
json.dumps(payload).encode(),
timeout=10.0,
)
return json.loads(reply.data)
except nats.errors.TimeoutError:
return {"error": "Device unreachable or SNMP timeout"}
except Exception as exc:
logger.error("SNMP discovery NATS request failed for %s: %s", ip_address, exc)
return {"error": str(exc)}
async def close() -> None:
"""Close the NATS connection. Called on application shutdown."""
global _nc
if _nc and not _nc.is_closed:
await _nc.drain()
_nc = None
logger.info("SNMP proxy NATS connection closed")