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",
|
"name": "satellite_devices",
|
||||||
"description": (
|
"description": (
|
||||||
"Listet die Geraete im Netz eines Satelliten (Fire TV, Chromecast, "
|
"Listet die Geraete im Netz eines Satelliten (Fire TV, Chromecast, "
|
||||||
"Smart-TVs, Drucker, NAS, Hosts ...). Nutze es um herauszufinden welches "
|
"Smart-TVs, Drucker, Switches, Router, APs, NAS, Hosts ...). Nutze es um "
|
||||||
"Geraet gemeint ist, BEVOR Du satellite_command aufrufst."
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -33,9 +33,14 @@ CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer
|
|||||||
|
|
||||||
# ─── SNMP (optional) ───────────────────────────────────────────────
|
# ─── SNMP (optional) ───────────────────────────────────────────────
|
||||||
# Defaults fuer die snmp.*-Aktionen; pro Request per params ueberschreibbar.
|
# 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_VERSION=2c # 1 | 2c
|
||||||
SNMP_TIMEOUT_SEC=5
|
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 (optional) ───────────────────────────────────────────────
|
||||||
HTTP_TIMEOUT_SEC=10 # Timeout fuer http.get/http.post
|
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_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_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
|
HEARTBEAT_SEC = 25
|
||||||
|
|
||||||
# mDNS-Servicetypen, die fuer ARIA interessant sind.
|
# mDNS-Servicetypen, die fuer ARIA interessant sind.
|
||||||
@@ -638,6 +655,44 @@ def _to_int(s: str):
|
|||||||
return None
|
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 ────────────────────────────────────────────────────────
|
# ─── Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _slug(s: str) -> str:
|
def _slug(s: str) -> str:
|
||||||
@@ -687,13 +742,42 @@ class Satellite:
|
|||||||
ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC)
|
ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC)
|
||||||
arp = await loop.run_in_executor(None, _discover_arp)
|
arp = await loop.run_in_executor(None, _discover_arp)
|
||||||
self._devices = _merge_devices(mdns, ssdp, arp)
|
self._devices = _merge_devices(mdns, ssdp, arp)
|
||||||
|
if SNMP_DISCOVERY:
|
||||||
|
await self._enrich_snmp(self._devices)
|
||||||
self._devices_ts = time.time()
|
self._devices_ts = time.time()
|
||||||
logger.info("[scan] %d Geraete (mdns=%d ssdp=%d arp=%d)",
|
n_snmp = sum(1 for d in self._devices if d.get("snmpCapable"))
|
||||||
len(self._devices), len(mdns), len(ssdp), len(arp))
|
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:
|
finally:
|
||||||
self._scanning = False
|
self._scanning = False
|
||||||
return self._devices
|
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:
|
async def _send(self, message: dict) -> None:
|
||||||
if self.ws is None:
|
if self.ws is None:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user