Später · 21 Neue Geräte im Heimnetz (Nachbartabelle des Proxmox-Hosts statt AdGuard)

AdGuard sieht in diesem Netz fast nur die Fritzbox, die die Anfragen der meisten Geräte weiterreicht (nachgesehen
26.09.). Deshalb:
- Ausführer: alle 30 min Ping an jede Adresse des /24 der Brücke vmbr0, dann ip neigh (MAC, IP, Rückwärtsname);
  nur lesend, im Bericht als host.netz (größer als /24 wird nicht abgesucht)
- services/homelab/netzgeraete.py: Register netz-bekannt.json; erster Abgleich = alles bekannt, Gäste des Hosts
  und neue MACs mit bekanntem Namen (Handys mit zufälliger MAC) gelten als bekannt; Neues einmal gemeldet
- Wächter: gelber Hinweis „Neues Gerät im Netz“ mit „Ist bekannt“; GET /api/homelab/netz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-26 09:43:45 +02:00
co-authored by Claude Opus 5.5
parent 241e91c03f
commit 5325544d89
6 changed files with 376 additions and 2 deletions
+9 -1
View File
@@ -134,6 +134,13 @@ def hinweis_aktion(hinweis_id: str, aktion_id: str) -> dict:
return _antwort(waechter.fuehre_aktion_aus(hinweis_id, aktion_id)) return _antwort(waechter.fuehre_aktion_aus(hinweis_id, aktion_id))
@router.get("/netz")
def netz() -> dict:
"""Die Geräte im Heimnetz aus der Nachbartabelle des Proxmox-Hosts, neue zuerst (seit 26.09.2026)."""
from services.homelab import netzgeraete
return netzgeraete.lage()
@router.get("/laeufe") @router.get("/laeufe")
def laeufe(anzahl: int = 20) -> dict: def laeufe(anzahl: int = 20) -> dict:
return {"laeufe": updates.laeufe(max(1, min(anzahl, 100)))} return {"laeufe": updates.laeufe(max(1, min(anzahl, 100)))}
@@ -375,12 +382,13 @@ def auftrag_abholen() -> dict:
@router.post("/ausfuehrer/bericht", dependencies=[Depends(_nur_ausfuehrer)]) @router.post("/ausfuehrer/bericht", dependencies=[Depends(_nur_ausfuehrer)])
def bericht(daten: dict) -> dict: def bericht(daten: dict) -> dict:
from services.homelab import entdecken, katalog, speicher from services.homelab import entdecken, katalog, netzgeraete, speicher
kanal.bericht_speichern(daten) kanal.bericht_speichern(daten)
speicher.verlauf_merken(daten) # Verlauf für „voll in etwa N Tagen“ (seit 25.09.2026) speicher.verlauf_merken(daten) # Verlauf für „voll in etwa N Tagen“ (seit 25.09.2026)
try: # „Neu entdeckt“ und der Katalog (seit 25.09.2026) halten den Bericht nie auf try: # „Neu entdeckt“ und der Katalog (seit 25.09.2026) halten den Bericht nie auf
entdecken.bericht_gesehen(daten) entdecken.bericht_gesehen(daten)
katalog.ergaenzen(daten) katalog.ergaenzen(daten)
netzgeraete.abgleichen(daten) # neue Geräte im Heimnetz (seit 26.09.2026)
threading.Thread(target=entdecken.docker_abgleichen, name="docker-abgleich", daemon=True).start() threading.Thread(target=entdecken.docker_abgleichen, name="docker-abgleich", daemon=True).start()
except Exception: except Exception:
log.warning("homelab: Abgleich mit dem Register gescheitert", exc_info=True) log.warning("homelab: Abgleich mit dem Register gescheitert", exc_info=True)
+166
View File
@@ -0,0 +1,166 @@
"""Neue Geräte im Heimnetz (Ausbauplan „Später“, Punkt 21, seit 26.09.2026).
Geplant war AdGuard als Quelle. Nachgesehen am 26.09.: AdGuard sieht in diesem Netz fast nur die Fritzbox, die die
Anfragen der meisten Geräte weiterreicht. Der Ausführer sucht deshalb alle 30 Minuten das Netz der Proxmox-Brücke ab
(Ping an jede Adresse, dann die Nachbartabelle; nur lesend) und schickt die Geräte mit MAC, IP und Namen im Bericht mit
(host.netz). Hier wird jede MAC mit dem Register <Datenordner>/netz-bekannt.json verglichen:
• Beim allerersten Abgleich gilt alles als bekannt — sonst stünden alle Geräte auf einmal da.
• Eine neue MAC wird „neu“: Lucy meldet sie einmal (nicht dringend, nachts mit der Morgenmeldung), und der Wächter
zeigt einen gelben Hinweis „Neues Gerät im Netz“ mit dem Knopf „Ist bekannt“, bis jemand ihn drückt.
• Handys wechseln ihre MAC (zufällige, „lokal verwaltete“ Adressen). Trägt eine neue MAC denselben Namen wie ein
bekanntes Gerät, gilt sie als dasselbe Gerät.
• Die Gäste des Proxmox-Hosts kennt der Orchestrator ohnehin; ihre IPs gelten als bekannt.
Verschwundene Geräte bleiben im Register (mit „zuletzt gesehen“): Ein Gerät, das nur ab und zu da ist, soll nicht jedes
Mal neu sein. Geschrieben wird nur im Prozess der Oberfläche (Bericht, Knopf); der Wächter im Steward liest.
"""
import json
import logging
import threading
import time
from datetime import datetime
from pathlib import Path
from kern.einstellungen import einstellungen
from kern.zeit import LOCAL_TZ
from services import announce
log = logging.getLogger(__name__)
BETREFF = "[Homelab]"
_lock = threading.Lock()
def _pfad() -> Path:
return einstellungen().daten_dir / "netz-bekannt.json"
def _lesen() -> dict | None:
try:
daten = json.loads(_pfad().read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return daten if isinstance(daten, dict) and isinstance(daten.get("geraete"), dict) else None
def _schreiben(daten: dict) -> None:
pfad = _pfad()
pfad.parent.mkdir(parents=True, exist_ok=True)
tmp = pfad.with_suffix(".tmp")
tmp.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8")
tmp.replace(pfad)
def lokal_verwaltet(mac: str) -> bool:
"""Zufällige MAC (zweites Bit des ersten Bytes gesetzt) — so schützen Handys ihre Privatsphäre."""
try:
return bool(int(mac.split(":")[0], 16) & 0x02)
except (ValueError, IndexError):
return False
def _kurzname(name: str | None) -> str | None:
return name.removesuffix(".fritz.box") if name else None
def abgleichen(bericht: dict, jetzt: float | None = None) -> list[dict]:
"""Nach jedem Bericht: Register nachführen. Rückgabe: die Geräte, die eben neu dazukamen (für die Meldung)."""
jetzt = time.time() if jetzt is None else jetzt
b = bericht.get("bericht") if isinstance(bericht.get("bericht"), dict) else bericht
netz = ((b or {}).get("host") or {}).get("netz")
if not isinstance(netz, dict) or not isinstance(netz.get("geraete"), list) or not netz.get("netz"):
return []
gast_ips = {str(g.get("ip")) for g in (b or {}).get("gaeste") or [] if isinstance(g, dict) and g.get("ip")}
neu: list[dict] = []
with _lock:
daten = _lesen()
erstes_mal = daten is None
daten = daten or {"geraete": {}}
register: dict = daten["geraete"]
namen = {e.get("name"): mac for mac, e in register.items() if e.get("name")}
for g in netz["geraete"]:
mac, ip, name = str(g.get("mac") or "").lower(), str(g.get("ip") or ""), g.get("name")
if not mac or not ip:
continue
if mac in register:
register[mac].update(ip=ip, zuletzt=jetzt, **({"name": name} if name else {}))
continue
gleicher_name = name and namen.get(name)
bekannt = erstes_mal or ip in gast_ips or bool(gleicher_name)
register[mac] = {"ip": ip, "name": name, "erstmals": jetzt, "zuletzt": jetzt, "bestaetigt": bekannt}
if gleicher_name:
register[mac]["wie"] = gleicher_name
if not bekannt:
neu.append({"mac": mac, **register[mac]})
daten["netz"], daten["stand"] = netz["netz"], float(netz.get("zeit") or jetzt)
_schreiben(daten)
for g in neu:
log.info("netz: neues Gerät %s (%s, %s)", g["ip"], g["mac"], g.get("name") or "ohne Namen")
if neu:
_melden(neu)
return neu
def _beschreibung(g: dict) -> str:
teile = [g["ip"], f"MAC {g['mac']}"]
if lokal_verwaltet(g["mac"]):
teile.append("zufällige MAC, oft ein Handy oder Tablet")
return " · ".join(teile)
def _melden(neu: list[dict]) -> None:
zeilen = [f"• {_kurzname(g.get('name')) or 'ohne Namen'} ({_beschreibung(g)})" for g in neu[:8]]
text = (("Ein neues Gerät ist" if len(neu) == 1 else f"{len(neu)} neue Geräte sind") + " im Heimnetz aufgetaucht:\n"
+ "\n".join(zeilen) + "\nKennst du es, drück im Homelab-Cockpit „Ist bekannt“.")
try:
announce.notify_telegram(BETREFF, text)
except Exception:
log.warning("netz: Meldung ging nicht raus", exc_info=True)
def bestaetigen(mac: str) -> dict:
"""Knopf „Ist bekannt“ (Aktion des Hinweises)."""
mac = str(mac or "").lower()
with _lock:
daten = _lesen()
eintrag = (daten or {}).get("geraete", {}).get(mac)
if not daten or not eintrag:
return {"ok": False, "detail": "Dieses Gerät kennt das Register nicht."}
eintrag["bestaetigt"] = True
_schreiben(daten)
return {"ok": True, "text": f"Gemerkt: {_kurzname(eintrag.get('name')) or eintrag['ip']} ist bekannt. Der Hinweis "
"verschwindet in wenigen Minuten."}
def lage() -> dict:
"""Für GET /api/homelab/netz: alle Geräte, neue zuerst, dann nach IP."""
daten = _lesen() or {"geraete": {}}
geraete = [{"mac": mac, **e, "name": _kurzname(e.get("name")), "zufaellig": lokal_verwaltet(mac)}
for mac, e in daten["geraete"].items()]
def ip_schluessel(g: dict) -> tuple:
try:
return tuple(int(t) for t in g["ip"].split("."))
except (ValueError, AttributeError):
return (999,)
geraete.sort(key=lambda g: (g.get("bestaetigt", True), ip_schluessel(g)))
return {"netz": daten.get("netz"), "stand": daten.get("stand"), "geraete": geraete}
def pruefe_netzgeraete() -> list:
"""Für den Wächter (Rolle homelab): jedes unbestätigte Gerät als gelber Hinweis mit dem Knopf „Ist bekannt“."""
from services.waechter import Befund, _aktion
befunde = []
for mac, e in ((_lesen() or {}).get("geraete") or {}).items():
if e.get("bestaetigt", True):
continue
g = {"mac": mac, **e}
seit = datetime.fromtimestamp(float(e.get("erstmals") or 0), LOCAL_TZ).strftime("%d.%m. %H:%M")
befunde.append(Befund(
id=f"netz:{mac}", stufe="gelb", titel=f"Neues Gerät im Netz: {_kurzname(e.get('name')) or e.get('ip')}",
text=f"{_beschreibung(g)}, zuerst gesehen {seit}. Kennst du es, drück „Ist bekannt“; sonst in der Fritzbox "
"nachsehen, wem es gehört.",
quelle="netz", aktionen=[_aktion("bekannt", "Ist bekannt", mac=mac)], sofort=True))
return befunde
+6
View File
@@ -640,6 +640,9 @@ PRUEFUNGEN += (__import__("services.homelab.speicher").homelab.speicher.pruefe_s
PRUEFUNGEN += (__import__("services.homelab.zertifikate").homelab.zertifikate.pruefe_zertifikate,) if ROLLE == "homelab" else () PRUEFUNGEN += (__import__("services.homelab.zertifikate").homelab.zertifikate.pruefe_zertifikate,) if ROLLE == "homelab" else ()
# Docker-Dienste unter Arcane (seit 25.09.2026): gelb, wenn einer ungesund ist oder ständig neu startet. # Docker-Dienste unter Arcane (seit 25.09.2026): gelb, wenn einer ungesund ist oder ständig neu startet.
PRUEFUNGEN += (__import__("services.homelab.docker").homelab.docker.pruefe_docker,) if ROLLE == "homelab" else () PRUEFUNGEN += (__import__("services.homelab.docker").homelab.docker.pruefe_docker,) if ROLLE == "homelab" else ()
# Neue Geräte im Heimnetz (seit 26.09.2026): gelb mit „Ist bekannt“, bis jemand ihn drückt.
PRUEFUNGEN += (__import__("services.homelab.netzgeraete").homelab.netzgeraete.pruefe_netzgeraete,) \
if ROLLE == "homelab" else ()
BETREFF_PROBLEM, BETREFF_OK = {"box": ("[Box-Problem]", "[Box wieder ok]"), BETREFF_PROBLEM, BETREFF_OK = {"box": ("[Box-Problem]", "[Box wieder ok]"),
"homelab": ("[Homelab-Problem]", "[Homelab wieder ok]")}[ROLLE] "homelab": ("[Homelab-Problem]", "[Homelab wieder ok]")}[ROLLE]
@@ -920,6 +923,9 @@ def fuehre_aktion_aus(hinweis_id: str, aktion_id: str) -> dict:
if not update_verlauf.freigeben(aktion["baustein"]): if not update_verlauf.freigeben(aktion["baustein"]):
return {"ok": False, "detail": "Der Baustein war schon freigegeben."} return {"ok": False, "detail": "Der Baustein war schon freigegeben."}
return {"ok": True, "text": update_verlauf.FREIGEGEBEN_TEXT} return {"ok": True, "text": update_verlauf.FREIGEGEBEN_TEXT}
if aktion_id == "bekannt": # neues Gerät im Heimnetz (seit 26.09.2026)
from services.homelab import netzgeraete
return netzgeraete.bestaetigen(str(aktion.get("mac") or ""))
if aktion_id == "ausblenden": if aktion_id == "ausblenden":
quittieren(hinweis_id, str(aktion.get("lauf", ""))) quittieren(hinweis_id, str(aktion.get("lauf", "")))
return {"ok": True, "text": "Ausgeblendet. Der Hinweis verschwindet in spätestens einer Minute " return {"ok": True, "text": "Ausgeblendet. Der Hinweis verschwindet in spätestens einer Minute "
+114
View File
@@ -0,0 +1,114 @@
"""Neue Geräte im Heimnetz (Ausbauplan „Später“, Punkt 21, 26.09.2026): Der Ausführer liest die Nachbartabelle des
Proxmox-Hosts, der Homelab-Teil führt ein Register und meldet Neues einmal; der Wächter zeigt es, bis „Ist bekannt“."""
import importlib.util
import json
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from kern import einstellungen as einstellungen_mod
from services.homelab import netzgeraete
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
T0 = 1_790_400_000.0
@pytest.fixture(scope="module")
def a():
spec = importlib.util.spec_from_file_location("ausfuehrer_netz", AUSFUEHRER)
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def test_netz_der_bruecke(a):
addr = json.dumps([{"ifname": "vmbr0", "addr_info": [{"family": "inet", "local": "192.168.178.108", "prefixlen": 24}]}])
assert str(a.netz_der_bruecke(addr)) == "192.168.178.0/24"
gross = json.dumps([{"addr_info": [{"family": "inet", "local": "10.0.0.5", "prefixlen": 16}]}])
assert a.netz_der_bruecke(gross) is None # größer als /24 wird nicht abgesucht
assert a.netz_der_bruecke("kaputt") is None
def test_nachbarn(a):
import ipaddress
neigh = json.dumps([
{"dst": "192.168.178.1", "lladdr": "74:42:7F:00:00:01", "state": ["REACHABLE"]},
{"dst": "192.168.178.23", "lladdr": "6e:81:03:00:00:02", "state": ["STALE"]},
{"dst": "192.168.178.40", "state": ["FAILED"]},
{"dst": "192.168.178.41", "lladdr": "aa:bb:cc:00:00:03", "state": ["INCOMPLETE"]},
{"dst": "fe80::1", "lladdr": "74:42:7f:00:00:01", "state": ["REACHABLE"]},
{"dst": "10.1.1.1", "lladdr": "11:22:33:00:00:04", "state": ["REACHABLE"]},
])
assert a.nachbarn(neigh, ipaddress.ip_network("192.168.178.0/24")) == [
{"ip": "192.168.178.1", "mac": "74:42:7f:00:00:01"}, {"ip": "192.168.178.23", "mac": "6e:81:03:00:00:02"}]
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
gemeldet: list[str] = []
monkeypatch.setattr(netzgeraete.announce, "notify_telegram", lambda betreff, text, **k: gemeldet.append(text))
yield gemeldet
einstellungen_mod.einstellungen.cache_clear()
def _bericht(*geraete: tuple[str, str, str | None], gaeste: tuple[str, ...] = ()) -> dict:
return {"host": {"netz": {"zeit": T0, "netz": "192.168.178.0/24",
"geraete": [{"ip": ip, "mac": mac, "name": name} for ip, mac, name in geraete]}},
"gaeste": [{"vmid": 100 + i, "ip": ip} for i, ip in enumerate(gaeste)]}
FRITZ = ("192.168.178.1", "74:42:7f:00:00:01", "fritz.box")
PC = ("192.168.178.22", "34:5a:60:00:00:05", "TobisNicerPC.fritz.box")
def test_erstes_mal_ist_alles_bekannt_dann_meldet_es_neues(daten):
assert netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0) == []
assert daten == [] and netzgeraete.pruefe_netzgeraete() == []
handy = ("192.168.178.23", "6e:81:03:00:00:02", None)
neu = netzgeraete.abgleichen(_bericht(FRITZ, PC, handy), jetzt=T0 + 1800)
assert [g["mac"] for g in neu] == ["6e:81:03:00:00:02"]
assert daten == [("Ein neues Gerät ist im Heimnetz aufgetaucht:\n• ohne Namen (192.168.178.23 · MAC "
"6e:81:03:00:00:02 · zufällige MAC, oft ein Handy oder Tablet)\nKennst du es, drück im "
"Homelab-Cockpit „Ist bekannt“.")]
(befund,) = netzgeraete.pruefe_netzgeraete()
assert (befund.id, befund.stufe, befund.titel) == ("netz:6e:81:03:00:00:02", "gelb", "Neues Gerät im Netz: 192.168.178.23")
assert befund.aktionen == [{"id": "bekannt", "label": "Ist bekannt", "mac": "6e:81:03:00:00:02"}] and befund.sofort
# Beim nächsten Bericht kein zweites Mal.
assert netzgeraete.abgleichen(_bericht(FRITZ, PC, handy), jetzt=T0 + 3600) == [] and len(daten) == 1
assert netzgeraete.bestaetigen("6E:81:03:00:00:02")["ok"]
assert netzgeraete.pruefe_netzgeraete() == []
assert not netzgeraete.bestaetigen("00:00:00:00:00:00")["ok"]
def test_gaeste_und_neue_mac_mit_bekanntem_namen_sind_nicht_neu(daten):
netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0)
gast = ("192.168.178.160", "bc:24:11:00:00:09", None)
pc_neue_mac = ("192.168.178.22", "36:5a:60:00:00:99", "TobisNicerPC.fritz.box")
assert netzgeraete.abgleichen(_bericht(FRITZ, gast, pc_neue_mac, gaeste=("192.168.178.160",)), jetzt=T0 + 60) == []
lage = netzgeraete.lage()
pc = next(g for g in lage["geraete"] if g["mac"] == "36:5a:60:00:00:99")
assert pc["wie"] == "34:5a:60:00:00:05" and pc["name"] == "TobisNicerPC" and pc["bestaetigt"]
def test_ohne_netz_im_bericht_passiert_nichts(daten):
assert netzgeraete.abgleichen({"host": {}}, jetzt=T0) == []
assert not (Path(einstellungen_mod.einstellungen().daten_dir) / "netz-bekannt.json").exists()
def test_schnittstelle_und_knopf_des_hinweises(daten, monkeypatch):
from routers import homelab
from services import waechter
netzgeraete.abgleichen(_bericht(FRITZ), jetzt=T0)
netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0 + 60)
app = FastAPI()
app.include_router(homelab.router)
lage = TestClient(app).get("/api/homelab/netz").json()
assert lage["netz"] == "192.168.178.0/24" and [g["bestaetigt"] for g in lage["geraete"]] == [False, True]
(befund,) = netzgeraete.pruefe_netzgeraete()
monkeypatch.setattr(waechter, "lese_stand", lambda: {"hinweise": [{"id": befund.id, "aktionen": befund.aktionen}]})
ergebnis = waechter.fuehre_aktion_aus(befund.id, "bekannt")
assert ergebnis["ok"] and ergebnis["text"].startswith("Gemerkt: TobisNicerPC ist bekannt.")
+80
View File
@@ -54,17 +54,20 @@ Aufruf: ausfuehrer.py Dauerbetrieb (systemd)
import argparse import argparse
import base64 import base64
import glob import glob
import ipaddress
import json import json
import logging import logging
import os import os
import platform import platform
import re import re
import socket
import subprocess import subprocess
import sys import sys
import threading import threading
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone from datetime import datetime, timezone
KONFIG = os.environ.get("MC2_AUSFUEHRER_KONFIG", "/etc/mc2-ausfuehrer.json") KONFIG = os.environ.get("MC2_AUSFUEHRER_KONFIG", "/etc/mc2-ausfuehrer.json")
@@ -1108,10 +1111,87 @@ def bericht() -> dict:
host["zertifikate"] = _zertifikate(gaeste) host["zertifikate"] = _zertifikate(gaeste)
except Exception as exc: except Exception as exc:
host["zertifikate_fehler"] = str(exc)[:200] host["zertifikate_fehler"] = str(exc)[:200]
try:
host["netz"] = _netz_stand()
except Exception as exc:
host["netz_fehler"] = str(exc)[:200]
return {"zeit": datetime.now(timezone.utc).isoformat(timespec="seconds"), "host": host, return {"zeit": datetime.now(timezone.utc).isoformat(timespec="seconds"), "host": host,
"gaeste": sorted(gaeste, key=lambda g: g["vmid"])} "gaeste": sorted(gaeste, key=lambda g: g["vmid"])}
# --- Geräte im Heimnetz (seit 26.09.2026, nur lesend) ----------------------------------------------------
# AdGuard sieht in diesem Netz fast nur die Fritzbox (sie reicht die Anfragen der meisten Geräte weiter). Wer im
# Heimnetz ist, verrät deshalb die Nachbartabelle dieses Hosts: Alle NETZ_ALLE_S ein Ping an jede Adresse des Netzes
# der Brücke (antwortet ein Gerät nicht auf Ping, beantwortet es trotzdem die ARP-Anfrage davor), dann `ip neigh`.
# Namen kommen aus der Rückwärtsauflösung (die Fritzbox kennt ihre DHCP-Namen). Größer als /24 wird nicht abgesucht.
NETZ_ALLE_S = 1800
NETZ_BRUECKE = "vmbr0"
_netz: dict = {"zeit": 0.0, "stand": None}
def netz_der_bruecke(addr_json: str) -> ipaddress.IPv4Network | None:
"""Das IPv4-Netz der Brücke aus `ip -j -4 addr show dev vmbr0`; None, wenn keins oder größer als /24."""
try:
for eintrag in json.loads(addr_json or "[]"):
for a in eintrag.get("addr_info") or []:
if a.get("family") in (None, "inet") and a.get("local") and int(a.get("prefixlen") or 0) >= 24:
return ipaddress.ip_interface(f"{a['local']}/{a['prefixlen']}").network
except (ValueError, TypeError, AttributeError):
return None
return None
def nachbarn(neigh_json: str, netz: ipaddress.IPv4Network) -> list[dict]:
"""Die Einträge von `ip -j neigh`, die eine MAC haben und im Netz liegen (FAILED/INCOMPLETE fallen weg)."""
geraete = {}
try:
eintraege = json.loads(neigh_json or "[]")
except ValueError:
return []
for e in eintraege:
zustand = set(e.get("state") or [])
mac = str(e.get("lladdr") or "").lower()
try:
ip = ipaddress.ip_address(str(e.get("dst")))
except ValueError:
continue
if not mac or zustand & {"FAILED", "INCOMPLETE"} or ip.version != 4 or ip not in netz:
continue
geraete[mac] = {"ip": str(ip), "mac": mac}
return sorted(geraete.values(), key=lambda g: ipaddress.ip_address(g["ip"]))
def _name(ip: str) -> str | None:
try:
return socket.gethostbyaddr(ip)[0]
except (OSError, UnicodeError):
return None
def netz_geraete() -> dict:
code, addr = _laufen(["ip", "-j", "-4", "addr", "show", "dev", NETZ_BRUECKE], 10)
netz = netz_der_bruecke(addr) if code == 0 else None
if netz is None:
return {"zeit": time.time(), "netz": None, "geraete": [], "grund": f"kein /24-Netz an {NETZ_BRUECKE}"}
with ThreadPoolExecutor(max_workers=64) as pool:
list(pool.map(lambda ip: _laufen(["ping", "-c", "1", "-W", "1", "-q", str(ip)], 5), netz.hosts()))
code, neigh = _laufen(["ip", "-j", "neigh", "show", "dev", NETZ_BRUECKE], 10)
geraete = nachbarn(neigh, netz) if code == 0 else []
with ThreadPoolExecutor(max_workers=16) as pool:
for g, name in zip(geraete, pool.map(_name, [g["ip"] for g in geraete]), strict=True):
g["name"] = name
return {"zeit": time.time(), "netz": str(netz), "geraete": geraete}
def _netz_stand() -> dict:
"""Höchstens alle NETZ_ALLE_S neu absuchen; dazwischen der letzte Stand."""
if _netz["stand"] is None or time.time() - _netz["zeit"] >= NETZ_ALLE_S:
_netz["stand"] = netz_geraete()
_netz["zeit"] = time.time()
return _netz["stand"]
# --- Aufträge (feste Liste) --------------------------------------------------------------- # --- Aufträge (feste Liste) ---------------------------------------------------------------
def _gast_pruefen(vmid: object) -> tuple[str, int]: def _gast_pruefen(vmid: object) -> tuple[str, int]:
+1 -1
View File
@@ -199,7 +199,7 @@ export const useHinweisAktion = (basis = "/api/hinweise") =>
post<AktionsErgebnis>(`${basis}/${encodeURIComponent(hinweis)}/aktion/${aktion}`), post<AktionsErgebnis>(`${basis}/${encodeURIComponent(hinweis)}/aktion/${aktion}`),
{ {
erfolg: (v, e) => erfolg: (v, e) =>
(v.aktion === "freigeben" || ZUSTAND_KNOEPFE.has(v.aktion)) && e.text (v.aktion === "freigeben" || v.aktion === "bekannt" || ZUSTAND_KNOEPFE.has(v.aktion)) && e.text
? e.text ? e.text
: `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`, : `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`,
neuLaden: basis === "/api/hinweise" ? ["start"] : ["homelab-hinweise"], neuLaden: basis === "/api/hinweise" ? ["start"] : ["homelab-hinweise"],