feat(satellite): SNMP-Anreicherung bei Discovery — Switches/Router/APs/NAS geben Infos preis
SNMP soll nicht nur Drucker abfragen: jedes beim Scan entdeckte Geraet mit IP wird jetzt kurz nach seiner SNMP-System-Group (RFC 1213) gefragt. Antwortet es, haengt der Satellit ein 'snmp'-Feld an (sysName/sysDescr/sysContact/sysLocation/ sysUpTime) und leitet einen praeziseren 'type' aus sysDescr ab (switch/router/ access-point/nas/printer/ups). Aus einer nackten ARP-IP wird so ein benanntes, klassifiziertes Geraet im Inventar, das ARIA via satellite_devices sieht. - parallel (Semaphore, Default 16) mit kurzem Timeout (-t1 -r0, 2s) -> Nicht- SNMP-Hosts fallen sofort raus, der Scan bleibt flott. - SNMP_DISCOVERY (Default true) schaltet es ab; Concurrency/Timeout per env. - satellite_devices-Tool: ARIA weiss jetzt vom snmp-Feld + genaueren type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+5
-2
@@ -1199,8 +1199,11 @@ META_TOOLS = [
|
||||
"name": "satellite_devices",
|
||||
"description": (
|
||||
"Listet die Geraete im Netz eines Satelliten (Fire TV, Chromecast, "
|
||||
"Smart-TVs, Drucker, NAS, Hosts ...). Nutze es um herauszufinden welches "
|
||||
"Geraet gemeint ist, BEVOR Du satellite_command aufrufst."
|
||||
"Smart-TVs, Drucker, Switches, Router, APs, NAS, Hosts ...). Nutze es um "
|
||||
"herauszufinden welches Geraet gemeint ist, BEVOR Du satellite_command "
|
||||
"aufrufst. SNMP-faehige Geraete tragen ein 'snmp'-Feld (Name, Beschreibung, "
|
||||
"Standort, Uptime) und einen praeziseren 'type' (switch/router/nas ...) — "
|
||||
"gute Quelle um Netz-Hardware zu erklaeren."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
||||
@@ -33,9 +33,14 @@ CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer
|
||||
|
||||
# ─── SNMP (optional) ───────────────────────────────────────────────
|
||||
# Defaults fuer die snmp.*-Aktionen; pro Request per params ueberschreibbar.
|
||||
SNMP_COMMUNITY=public # Drucker antworten meist auf 'public'
|
||||
SNMP_COMMUNITY=public # Drucker/Switches antworten meist auf 'public'
|
||||
SNMP_VERSION=2c # 1 | 2c
|
||||
SNMP_TIMEOUT_SEC=5
|
||||
# Discovery-Anreicherung: jedes entdeckte Geraet wird beim Scan kurz per SNMP
|
||||
# nach Name/Beschreibung/Standort/Uptime gefragt (Switches, Router, APs, NAS ...).
|
||||
SNMP_DISCOVERY=true
|
||||
SNMP_DISCOVERY_CONCURRENCY=16 # parallele SNMP-Abfragen pro Scan
|
||||
SNMP_DISCOVERY_TIMEOUT=2 # Timeout je Geraet (s) — Nicht-SNMP-Hosts fallen schnell raus
|
||||
|
||||
# ─── HTTP (optional) ───────────────────────────────────────────────
|
||||
HTTP_TIMEOUT_SEC=10 # Timeout fuer http.get/http.post
|
||||
|
||||
+86
-2
@@ -139,6 +139,23 @@ SNMP_SUPPLY_DESC = "1.3.6.1.2.1.43.11.1.1.6.1" # Beschreibung (z.B. "Black Ink
|
||||
SNMP_SUPPLY_MAX = "1.3.6.1.2.1.43.11.1.1.8.1" # Max-Kapazitaet
|
||||
SNMP_SUPPLY_LVL = "1.3.6.1.2.1.43.11.1.1.9.1" # aktueller Fuellstand
|
||||
|
||||
# SNMP-Anreicherung bei der Discovery: jedes entdeckte Geraet mit IP wird kurz
|
||||
# nach seiner System-Group (RFC 1213) gefragt. Switches/Router/APs/NAS geben so
|
||||
# Name, Beschreibung, Standort & Uptime preis -> im Inventar (satellite_devices)
|
||||
# sichtbar. Abschaltbar; kurzer Timeout + parallel, damit der Scan flott bleibt.
|
||||
SNMP_DISCOVERY = _env_bool("SNMP_DISCOVERY", True)
|
||||
SNMP_DISCOVERY_CONCURRENCY = int(os.environ.get("SNMP_DISCOVERY_CONCURRENCY", "16") or "16")
|
||||
SNMP_DISCOVERY_TIMEOUT = float(os.environ.get("SNMP_DISCOVERY_TIMEOUT", "2") or "2")
|
||||
# System-Group (RFC 1213) .0-Instanzen:
|
||||
SNMP_SYS_OIDS = {
|
||||
"descr": "1.3.6.1.2.1.1.1.0", # sysDescr
|
||||
"objectid": "1.3.6.1.2.1.1.2.0", # sysObjectID
|
||||
"uptime": "1.3.6.1.2.1.1.3.0", # sysUpTime
|
||||
"contact": "1.3.6.1.2.1.1.4.0", # sysContact
|
||||
"name": "1.3.6.1.2.1.1.5.0", # sysName
|
||||
"location": "1.3.6.1.2.1.1.6.0", # sysLocation
|
||||
}
|
||||
|
||||
HEARTBEAT_SEC = 25
|
||||
|
||||
# mDNS-Servicetypen, die fuer ARIA interessant sind.
|
||||
@@ -638,6 +655,44 @@ def _to_int(s: str):
|
||||
return None
|
||||
|
||||
|
||||
def _snmp_system(ip: str, community: str = "", version: str = "") -> Optional[dict]:
|
||||
"""Fragt die SNMP-System-Group eines Hosts ab (ein snmpget, alle 6 OIDs).
|
||||
Gibt {descr,name,contact,location,uptime,objectid} oder None (kein SNMP).
|
||||
Kurzer Timeout, keine Retries -> Nicht-SNMP-Hosts scheitern schnell."""
|
||||
base = ["-v", str(version or SNMP_VERSION), "-c", str(community or SNMP_COMMUNITY),
|
||||
"-t", "1", "-r", "0"]
|
||||
keys = list(SNMP_SYS_OIDS.keys())
|
||||
ok, out = _snmp_run(["snmpget", "-Oqv", *base, ip, *SNMP_SYS_OIDS.values()],
|
||||
SNMP_DISCOVERY_TIMEOUT)
|
||||
if not ok:
|
||||
return None
|
||||
vals = out.splitlines()
|
||||
info = {}
|
||||
for k, v in zip(keys, vals):
|
||||
v = (v or "").strip().strip('"')
|
||||
if v and "No Such" not in v and "No more" not in v:
|
||||
info[k] = v
|
||||
return info or None
|
||||
|
||||
|
||||
def _snmp_kind(descr: str) -> str:
|
||||
"""Grobe Geraeteklasse aus sysDescr (fuer type im Inventar)."""
|
||||
d = (descr or "").lower()
|
||||
if any(k in d for k in ("switch", "catalyst", "procurve", "aruba", "powerconnect")):
|
||||
return "switch"
|
||||
if any(k in d for k in ("router", "mikrotik", "routeros", "edgeos", "openwrt", "pfsense", "fritz!box")):
|
||||
return "router"
|
||||
if any(k in d for k in ("access point", "accesspoint", "unifi", "wifi", "wlan")):
|
||||
return "access-point"
|
||||
if any(k in d for k in ("printer", "laserjet", "officejet", "brother", "epson", "kyocera")):
|
||||
return "printer"
|
||||
if any(k in d for k in ("nas", "synology", "qnap", "truenas", "diskstation")):
|
||||
return "nas"
|
||||
if any(k in d for k in ("ups", "usv", "smart-ups")):
|
||||
return "ups"
|
||||
return ""
|
||||
|
||||
|
||||
# ─── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
def _slug(s: str) -> str:
|
||||
@@ -687,13 +742,42 @@ class Satellite:
|
||||
ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC)
|
||||
arp = await loop.run_in_executor(None, _discover_arp)
|
||||
self._devices = _merge_devices(mdns, ssdp, arp)
|
||||
if SNMP_DISCOVERY:
|
||||
await self._enrich_snmp(self._devices)
|
||||
self._devices_ts = time.time()
|
||||
logger.info("[scan] %d Geraete (mdns=%d ssdp=%d arp=%d)",
|
||||
len(self._devices), len(mdns), len(ssdp), len(arp))
|
||||
n_snmp = sum(1 for d in self._devices if d.get("snmpCapable"))
|
||||
logger.info("[scan] %d Geraete (mdns=%d ssdp=%d arp=%d, snmp=%d)",
|
||||
len(self._devices), len(mdns), len(ssdp), len(arp), n_snmp)
|
||||
finally:
|
||||
self._scanning = False
|
||||
return self._devices
|
||||
|
||||
async def _enrich_snmp(self, devices: list) -> None:
|
||||
"""Fragt jedes Geraet mit IP parallel per SNMP-System-Group ab und haengt
|
||||
die Infos an. Verbessert Name/Typ, wenn bisher nur eine IP bekannt war."""
|
||||
loop = asyncio.get_event_loop()
|
||||
sem = asyncio.Semaphore(SNMP_DISCOVERY_CONCURRENCY)
|
||||
|
||||
async def _one(dev: dict) -> None:
|
||||
ip = dev.get("ip") or ""
|
||||
if not ip:
|
||||
return
|
||||
async with sem:
|
||||
info = await loop.run_in_executor(None, _snmp_system, ip)
|
||||
if not info:
|
||||
return
|
||||
dev["snmp"] = info
|
||||
dev["snmpCapable"] = True
|
||||
# Name aufwerten, wenn er bisher nur die IP/leer war.
|
||||
if info.get("name") and dev.get("name", "") in ("", ip):
|
||||
dev["name"] = info["name"]
|
||||
# Typ aufwerten, wenn bisher generisch (host/leer).
|
||||
kind = _snmp_kind(info.get("descr", ""))
|
||||
if kind and dev.get("type", "") in ("", "host"):
|
||||
dev["type"] = kind
|
||||
|
||||
await asyncio.gather(*(_one(d) for d in devices))
|
||||
|
||||
async def _send(self, message: dict) -> None:
|
||||
if self.ws is None:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user