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>
This commit is contained in:
112
backend/app/services/snmp_proxy.py
Normal file
112
backend/app/services/snmp_proxy.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user