Jede Gerätekachel (Homelab-Cockpit und Übersicht) zeigt eine Live-Zahl, wo es eine gibt: Proxmox-Host „8 von 8 Gästen an“, PBS „letzte Sicherung vor 13 h“, NPMplus „10 Zertifikate gültig“ (aus der Zertifikats-Wache), Arcane „11 von 12 Containern an“ (Arcane-API mit dem vorhandenen Schlüssel) und AdGuard „12 % geblockt“. Für AdGuard gibt es unter Einstellungen → Homelab einen Zugang, der vor dem Speichern gegen AdGuard geprüft wird und nur im Homelab-Teil liegt (0600, das Passwort steht in keiner Antwort). Arcane und AdGuard werden höchstens alle zwei Minuten gefragt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
7.7 KiB
Python
149 lines
7.7 KiB
Python
"""Live-Zahlen je Gerät auf den Kacheln (Ausbauplan Welle 1, 25.09.2026) und der AdGuard-Zugang dafür."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from kern import einstellungen as einstellungen_mod
|
|
from services.homelab import adguard, arcane, kanal, kennzahlen, zertifikate
|
|
|
|
BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8"))
|
|
PASSWORT = "geheim-Passwort-123"
|
|
|
|
|
|
@pytest.fixture
|
|
def daten(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
adguard._cache.clear()
|
|
arcane.cache_leeren()
|
|
yield tmp_path
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
adguard._cache.clear()
|
|
arcane.cache_leeren()
|
|
|
|
|
|
def _bericht() -> dict:
|
|
b = json.loads(json.dumps(BERICHT))
|
|
b["host"]["sicherungsziele"] = {"pbs-qnap": {"neueste": {"102": 1790296227, "104": 1790296257, "106": 1790296281}}}
|
|
return b
|
|
|
|
|
|
def test_zahlen_je_geraet(monkeypatch):
|
|
npm = {"quelle": "npm", "gast": 101, "stufe": "ok"}
|
|
monkeypatch.setattr(zertifikate, "lage", lambda jetzt=None: {"zertifikate": [npm] * 10})
|
|
monkeypatch.setattr(arcane, "container_zahlen", lambda url: {"laufend": 11, "gesamt": 12})
|
|
monkeypatch.setattr(adguard, "geblockt", lambda: {"anfragen": 84_211, "geblockt": 10_523, "anteil": 12.5})
|
|
zahlen = kennzahlen.fuer_bericht(_bericht())
|
|
laufend = sum(1 for g in BERICHT["gaeste"] if g["status"] == "running")
|
|
assert zahlen["pve"] == {"text": f"{laufend} von {len(BERICHT['gaeste'])} Gästen an"}
|
|
assert zahlen["ct-105"] == {"text": "letzte Sicherung", "zeit": 1790296281}
|
|
assert zahlen["ct-101"] == {"text": "10 Zertifikate gültig"}
|
|
assert zahlen["vm-106"] == {"text": "11 von 12 Containern an"}
|
|
assert zahlen["ct-100"] == {"text": "12 % geblockt"}
|
|
assert "ct-104" not in zahlen # Gitea: keine Zahl
|
|
|
|
monkeypatch.setattr(zertifikate, "lage", lambda jetzt=None: {"zertifikate": [npm, {**npm, "stufe": "gelb"}]})
|
|
monkeypatch.setattr(adguard, "geblockt", lambda: {"anteil": 4.25})
|
|
monkeypatch.setattr(arcane, "container_zahlen", lambda url: (_ for _ in ()).throw(RuntimeError("weg")))
|
|
zahlen = kennzahlen.fuer_bericht(_bericht())
|
|
assert zahlen["ct-101"] == {"text": "1 von 2 Zertifikaten mit Problem", "stufe": "gelb"}
|
|
assert zahlen["ct-100"] == {"text": "4,2 % geblockt"} and "vm-106" not in zahlen # ein Fehler kostet nur die Zahl
|
|
|
|
|
|
def test_ziele_tragen_die_kennzahl(daten, monkeypatch):
|
|
from services.homelab import inventar
|
|
monkeypatch.setattr(inventar, "erreichbar", lambda url: True)
|
|
monkeypatch.setattr(inventar, "neueste_version", lambda repo: None)
|
|
monkeypatch.setattr(kennzahlen, "fuer_bericht", lambda b: {"pve": {"text": "8 von 8 Gästen an"}})
|
|
kanal.bericht_speichern(_bericht())
|
|
ziele = {z["id"]: z for z in inventar.ziele()["ziele"]}
|
|
assert ziele["pve"]["kennzahl"] == {"text": "8 von 8 Gästen an"} and ziele["ct-104"]["kennzahl"] is None
|
|
monkeypatch.setattr(kennzahlen, "fuer_bericht", lambda b: 1 / 0)
|
|
assert all(z["kennzahl"] is None for z in inventar.ziele()["ziele"]) # die Liste hängt nie daran
|
|
|
|
|
|
def test_arcane_zaehlt_ueber_alle_umgebungen(daten, monkeypatch):
|
|
monkeypatch.setenv("MC_ARCANE_KEY", "schluessel-123")
|
|
monkeypatch.setattr(arcane, "umgebungen", lambda url, mit_schluessel=None: [{"id": 0}, {"id": 1}])
|
|
antworten = {0: {"data": {"runningContainers": 9, "totalContainers": 10}},
|
|
1: {"data": {"runningContainers": 2, "totalContainers": 2}}}
|
|
monkeypatch.setattr(arcane, "_get", lambda url, pfad, s=None: antworten[int(pfad.split("/")[2])])
|
|
assert arcane.container_zahlen("http://arcane:3552") == {"laufend": 11, "gesamt": 12}
|
|
monkeypatch.delenv("MC_ARCANE_KEY")
|
|
arcane.cache_leeren()
|
|
assert arcane.container_zahlen("http://arcane:3552") is None # ohne Schlüssel: nichts
|
|
|
|
|
|
class _Antwort:
|
|
def __init__(self, status: int, daten: object):
|
|
self.status_code, self._daten = status, daten
|
|
|
|
def raise_for_status(self):
|
|
if self.status_code >= 400:
|
|
raise httpx.HTTPStatusError("x", request=httpx.Request("GET", "http://x"), response=self)
|
|
|
|
def json(self):
|
|
return self._daten
|
|
|
|
|
|
def test_adguard_statistik(monkeypatch):
|
|
gesehen = []
|
|
monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: gesehen.append((url, auth)) or _Antwort(
|
|
200, {"num_dns_queries": 1000, "num_blocked_filtering": 125}))
|
|
assert adguard.statistik("http://192.168.178.100:8080", "orchestrator", PASSWORT) == {
|
|
"ok": True, "anfragen": 1000, "geblockt": 125, "anteil": 12.5}
|
|
assert gesehen == [("http://192.168.178.100:8080/control/stats", ("orchestrator", PASSWORT))]
|
|
monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: _Antwort(401, {}))
|
|
assert adguard.statistik("http://x", "a", "b") == {"ok": False, "status": 401, "art": "abgelehnt"}
|
|
|
|
def weg(url, auth, timeout):
|
|
raise httpx.ConnectError("weg")
|
|
|
|
monkeypatch.setattr(adguard.httpx, "get", weg)
|
|
assert adguard.statistik("http://x", "a", "b")["art"] == "netz"
|
|
monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: _Antwort(200, {"anders": 1}))
|
|
assert adguard.statistik("http://x", "a", "b")["art"] == "antwort"
|
|
|
|
|
|
def test_adguard_zugang_erst_pruefen_dann_speichern(daten, monkeypatch):
|
|
from routers import homelab
|
|
kanal.bericht_speichern(_bericht())
|
|
app = FastAPI()
|
|
app.include_router(homelab.router)
|
|
client = TestClient(app)
|
|
antworten = {"ok": False, "status": 401, "art": "abgelehnt"}
|
|
monkeypatch.setattr(adguard, "statistik", lambda url, b, p: antworten)
|
|
abgelehnt = client.post("/api/homelab/einstellungen/adguard-zugang",
|
|
json={"benutzer": "orchestrator", "passwort": PASSWORT}).json()
|
|
assert abgelehnt == {"ok": False, "detail": "AdGuard lehnt Benutzer oder Passwort ab (HTTP 401). Nichts gespeichert."}
|
|
assert adguard.zugang() is None
|
|
antworten = {"ok": True, "anfragen": 1000, "geblockt": 125, "anteil": 12.5}
|
|
monkeypatch.setattr(adguard, "statistik", lambda url, b, p: antworten)
|
|
gut = client.post("/api/homelab/einstellungen/adguard-zugang",
|
|
json={"benutzer": " orchestrator ", "passwort": PASSWORT}).json()
|
|
assert gut["ok"] is True and "davon 12,5 % geblockt" in gut["text"] and PASSWORT not in json.dumps(gut)
|
|
assert adguard.zugang() == ("orchestrator", PASSWORT)
|
|
if os.name != "nt":
|
|
assert oct((daten / "adguard.json").stat().st_mode & 0o777) == "0o600"
|
|
stand = client.get("/api/homelab/einstellungen").json()["adguard"]
|
|
assert stand == {"url": "http://192.168.178.100:8080", "zugang": "hinterlegt", "benutzer": "orchestrator"}
|
|
assert PASSWORT not in json.dumps(client.get("/api/homelab/einstellungen").json())
|
|
kaputt = client.post("/api/homelab/einstellungen/adguard-zugang", content=b"passwort=" + PASSWORT.encode())
|
|
assert kaputt.json()["ok"] is False and PASSWORT not in kaputt.text
|
|
assert client.delete("/api/homelab/einstellungen/adguard-zugang").json()["text"] == "Der AdGuard-Zugang ist gelöscht."
|
|
assert adguard.zugang() is None
|
|
|
|
|
|
def test_adguard_kachel_fragt_hoechstens_alle_zwei_minuten(daten, monkeypatch):
|
|
kanal.bericht_speichern(_bericht())
|
|
adguard.zugang_speichern("orchestrator", PASSWORT)
|
|
fragen = []
|
|
monkeypatch.setattr(adguard, "statistik", lambda url, b, p: fragen.append(url) or {"ok": True, "anteil": 7.0})
|
|
assert adguard.geblockt() == {"ok": True, "anteil": 7.0}
|
|
assert adguard.geblockt() == {"ok": True, "anteil": 7.0} and len(fragen) == 1
|