Files
mission-control-v2/backend/tests/test_netzgeraete.py
T
HitonabiandClaude Opus 5.5 5325544d89 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>
2026-09-26 09:43:45 +02:00

115 lines
5.7 KiB
Python

"""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.")