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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
363bc211e1
commit
9da666d04e
@@ -0,0 +1,118 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user