Files
mission-control-v2/backend/tests/test_homelab_wartung.py
T
HitonabiandClaude Opus 5.5 9da666d04e Welle 3 · Wartungsfenster und Update-Regel je Gerät (So 03:00–04:15, Start: Sicherheitsupdates automatisch)
Der Steward des Homelab-Teils prüft jede Minute, ob das Fenster offen ist, und arbeitet dann einmal je Tag den Plan ab:
je Gerät nach seiner Regel — nur melden, Sicherheitsupdates automatisch (Standard, User-Entscheid) oder alles im
Fenster —, mit den bekannten Update-Läufen (Rückweg vorher, Prüfung danach, bei Rot zurück). Nach dem Fenster beginnt
kein Schritt mehr, Erfolge kommen als eine Meldung am Ende, Fehler sofort; der Proxmox-Host startet nie von selbst neu.
Der Ausführer meldet dafür die Namen der Sicherheitspakete und spielt auf Wunsch nur diese ein (Gäste und Host);
neuer Baustein „sicherheit“. Dazu „Version überspringen“ je App, die Einstellungen unter Einstellungen → Homelab und
„Wartung“ statt „Störung“ im Cockpit, solange das Fenster läuft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:38 +02:00

119 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Wartungsfenster und Update-Regel je Gerät (Ausbauplan Welle 3, Punkt 17, 25.09.2026). User-Entscheid: So 03:00–04:15,
zu Beginn für alle Geräte „Sicherheitsupdates automatisch“, alles andere nur melden; der Host startet nie von selbst."""
from datetime import datetime
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from kern import einstellungen as einstellungen_mod
from kern.zeit import LOCAL_TZ
from services.homelab import wartung
SONNTAG_0310 = datetime(2026, 9, 27, 3, 10, tzinfo=LOCAL_TZ).timestamp()
SONNTAG_0420 = datetime(2026, 9, 27, 4, 20, tzinfo=LOCAL_TZ).timestamp()
SAMSTAG_0310 = datetime(2026, 9, 26, 3, 10, tzinfo=LOCAL_TZ).timestamp()
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
yield tmp_path
einstellungen_mod.einstellungen.cache_clear()
def _b(bid: str, zustand: str = "neu", sicherheit: int | None = None, aktion: bool = True) -> dict:
return {"id": bid, "zustand": zustand, "sicherheit": sicherheit, "aktion": {"pfad": "/x"} if aktion else None}
ZIELE = [
{"id": "pve", "name": "Proxmox-Host", "bausteine": [_b("pakete", sicherheit=2), _b("neustart")]},
{"id": "ct-100", "name": "AdGuard Home", "bausteine": [_b("os", sicherheit=0)]},
{"id": "ct-104", "name": "Gitea", "bausteine": [_b("app"), _b("os", sicherheit=3)]},
{"id": "vm-106", "name": "Arcane (Docker)", "bausteine": [_b("docker")]},
]
def test_fenster(daten):
assert wartung.im_fenster(SONNTAG_0310) and not wartung.im_fenster(SONNTAG_0420)
assert not wartung.im_fenster(SAMSTAG_0310)
assert wartung.fenster_text() == "So 03:00–04:15"
assert wartung.naechstes(SAMSTAG_0310) == datetime(2026, 9, 27, 3, 0, tzinfo=LOCAL_TZ).timestamp()
assert wartung.naechstes(SONNTAG_0310) == datetime(2026, 9, 27, 3, 0, tzinfo=LOCAL_TZ).timestamp() # läuft gerade
assert wartung.naechstes(SONNTAG_0420) == datetime(2026, 10, 4, 3, 0, tzinfo=LOCAL_TZ).timestamp()
def test_plan_nach_regel(daten):
# Standard „sicherheit“: nur Sicherheitsupdates, der Host ohne Neustart.
assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [("pve", "sicherheit"), ("ct-104", "sicherheit")]
wartung.setzen({"regeln": {"ct-104": "alles", "pve": "melden", "vm-106": "alles"}})
assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [
("ct-104", "app"), ("ct-104", "os"), ("vm-106", "docker")]
wartung.setzen({"regeln": {"ct-104": None}, "standard": "melden"})
assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [("vm-106", "docker")]
def test_einstellungen_pruefen(daten):
assert wartung.setzen({"fenster": {"tag": 6, "von": "05:00", "bis": "04:00"}})["ok"] is False
assert wartung.setzen({"fenster": {"tag": 9, "von": "03:00", "bis": "04:00"}})["ok"] is False
assert wartung.setzen({"regeln": {"ct-104": "immer"}})["ok"] is False
assert wartung.setzen({"regeln": {"rm -rf": "alles"}})["ok"] is False
gut = wartung.setzen({"fenster": {"tag": 5, "von": "02:30", "bis": "04:00"}})
assert gut == {"ok": True, "text": "Gespeichert. Wartungsfenster Sa 02:30–04:00."}
def test_takt_einmal_je_sonntag_und_nie_neben_einem_update(daten, monkeypatch):
from services.homelab import inventar, sammellauf, updates
monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": ZIELE})
monkeypatch.setattr(sammellauf, "laeuft", lambda: False)
laufend = [{"ct-100"}]
monkeypatch.setattr(updates, "laufende_ziele", lambda: laufend[0])
gestartet: list[dict] = []
monkeypatch.setattr(wartung.threading, "Thread", lambda target, args, name, daemon: type(
"T", (), {"start": lambda self: gestartet.append(args[0])})())
assert wartung.takt(SAMSTAG_0310) is False
assert wartung.takt(SONNTAG_0310) is False and gestartet == [] # ein Update läuft: nächste Minute
laufend[0] = set()
assert wartung.takt(SONNTAG_0310) is True and len(gestartet) == 1
assert wartung.takt(SONNTAG_0310 + 60) is False # heute schon gelaufen
assert wartung.lage(SONNTAG_0310)["stand"]["schritte"][0]["baustein"] == "sicherheit"
def test_abarbeiten_und_zusammenfassung(daten, monkeypatch):
from services.homelab import updates
monkeypatch.setattr(wartung, "im_fenster", lambda jetzt=None, fenster=None: True)
gestartet: list[tuple] = []
ergebnisse = {"pve": "eingespielt", "ct-104": "zurueckgerollt"}
def starten(zid, baustein, sammellauf=None):
gestartet.append((zid, baustein, sammellauf["anlass"]))
return {"ok": True, "lauf": zid}
monkeypatch.setattr(updates, "starten", starten)
monkeypatch.setattr(updates, "lauf_lesen", lambda lid: {"status": "fertig", "ergebnis": ergebnisse[lid], "text": "…"})
meldungen: list[str] = []
monkeypatch.setattr(wartung, "_melden", meldungen.append)
stand = {"datum": "2026-09-27", "status": "laeuft", "start": SONNTAG_0310, "schritte": wartung.plan(ZIELE)}
wartung._abarbeiten(stand)
assert gestartet == [("pve", "sicherheit", "Wartungsfenster"), ("ct-104", "sicherheit", "Wartungsfenster")]
assert wartung.stand_lesen()["status"] == "fertig"
assert meldungen == [("Wartung So 03:00–04:15: Eingespielt: Proxmox-Host (Sicherheitsupdates). Zurückgerollt (läuft "
"wie vorher): Gitea (Sicherheitsupdates).")]
def test_version_ueberspringen_und_schnittstelle(daten):
from routers import homelab
app = FastAPI()
app.include_router(homelab.router)
client = TestClient(app)
assert client.post("/api/homelab/ziele/ct-104/ueberspringen", json={"version": "1.27.4"}).json()["ok"] is True
assert wartung.uebersprungen("ct-104", "app") == "1.27.4"
lage = client.get("/api/homelab/wartung").json()
assert (lage["fenster_text"], lage["standard"], lage["uebersprungen"]) == (
"So 03:00–04:15", "sicherheit", {"ct-104:app": "1.27.4"})
assert client.delete("/api/homelab/ziele/ct-104/ueberspringen").json()["text"] == "Die Version wird wieder angeboten."
assert wartung.uebersprungen("ct-104", "app") is None
assert client.post("/api/homelab/wartung", json={"regeln": {"ct-104": "alles"}}).json()["ok"] is True
assert client.post("/api/homelab/wartung", json={"standard": "nie"}).status_code == 409