homelab: Wartezeit nach Skriptaenderung, PBS mit Sicherung statt Snapshot, woechentliches Suchen

- karenz.py: ct/<app>.sh juenger als MC_HOMELAB_KARENZ_H (Standard 48 h) -> Baustein "neu" ohne Knopf,
  updates.starten lehnt mit demselben Satz ab (Berliner Zeit). GitHub stumm -> nicht blockieren, die
  Rueckfrage sagt es.
- Ausfuehrer: neue Aktionen sichern, sicherung_zurueck, sicherung_loeschen. vzdump auf den ersten lokalen
  Speicher mit Inhalt backup (oder sicherung_speicher aus /etc/mc2-ausfuehrer.json), nie auf pbs; vorher
  Platz pruefen (frei > belegt x 1,2); Notiz mc2-sicherung, nur solche werden zurueckgespielt/geloescht;
  Rueckweg: stoppen, pct restore --force auf den bisherigen rootfs-Speicher, starten. Bericht mit
  host.sicherung, sicherung_moeglich/_grund, eigenen Sicherungen und nur_lesen. Unerwartete Fehler werden
  beantwortet statt verschluckt; vzdump/restore beim Zeitlimit erst SIGTERM.
- updates.py: Sicherung, wo kein Snapshot geht; bei Rot zurueckspielen, nach Gruen aeltere Sicherungen
  weg. Scheitert Snapshot oder Sicherung, beginnt das Update nicht (nicht dringend gemeldet).
- pflege.py: "suchen" fuer Gaeste mit Paketlisten aelter als 7 Tage, nachts 02-05 Uhr (sonst nachholen),
  einmal je Gast und Tag, nie neben einem Update oder offenen Auftrag; gelber Waechter-Hinweis, wenn es
  zweimal hintereinander scheitert. Eine Registrierungszeile in waechter.py.
- kanal.py: Auftragsliste unter flock, weil jetzt auch der Steward Auftraege anlegt.
- inventar.py: Rueckweg und Rueckfrage fuer die Sicherung; Gaeste mitten im Update-Lauf nicht in der
  Webpruefung des Waechters (sonst zweiter Alarm beim Zurueckspielen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 20:22:39 +02:00
co-authored by Claude Opus 5.5
parent 40d2840b16
commit f2733f44fe
13 changed files with 1385 additions and 54 deletions
+439
View File
@@ -0,0 +1,439 @@
"""PBS: Sicherung statt Snapshot (24.09.2026). Der Ausführer über Fakes (die Speicher und CT 105 so, wie sie am
24.09. auf dem Proxmox-Host standen: pvesm status, storage.cfg, pct config 105), der Ablauf im Homelab-Teil mit
einem nachgespielten Ausführer."""
import importlib.util
import json
import threading
import time
from pathlib import Path
import pytest
from kern import einstellungen as einstellungen_mod
from services.homelab import inventar, kanal, updates
BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8"))
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
GIB = 1024 ** 3
# /nodes/pve/storage am 24.09.2026 (Zahlen aus pvesm status, KiB → Bytes).
SPEICHER = [
{"storage": "local", "type": "dir", "content": "vztmpl,backup,iso", "active": 1, "enabled": 1, "shared": 0,
"avail": 46835092 * 1024, "used": 46613140 * 1024, "total": 98497780 * 1024},
{"storage": "local-lvm", "type": "lvmthin", "content": "rootdir,images", "active": 1, "enabled": 1, "shared": 0,
"avail": 186793893 * 1024},
{"storage": "pbs-qnap", "type": "pbs", "content": "backup", "active": 1, "enabled": 1, "shared": 1,
"avail": 2282130304 * 1024},
]
KONFIG_105 = {"rootfs": "local-lvm:vm-105-disk-0,size=10G", "mp0": "/mnt/qnap-backup/pbs-datastore,mp=/mnt/datastore",
"unprivileged": 1, "onboot": 1, "ostype": "debian", "tags": "backup;community-script"}
ALT = "local:backup/vzdump-lxc-105-2026_09_20-03_00_00.tar.zst"
NOTIZ = "mc2-sicherung: vor einem Update durch den Homelab Orchestrator (proxmox-backup-server)"
def _modul():
spec = importlib.util.spec_from_file_location("ausfuehrer", AUSFUEHRER)
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
# --- Reine Auswertungen im Ausführer ----------------------------------------------------------
def test_speicher_wahl_nimmt_local_und_nie_den_pbs():
a = _modul()
s, fehlt = a.sicherungsspeicher(SPEICHER)
assert s["storage"] == "local" and fehlt is None
# Ohne lokalen Speicher mit Sicherungen: klare Ablehnung, der PBS ist keine Ausweichlösung.
s, fehlt = a.sicherungsspeicher([x for x in SPEICHER if x["storage"] != "local"])
assert s is None and "Kein lokaler Speicher" in fehlt and "VZDump-Sicherung" in fehlt
assert a.sicherungsspeicher([])[1].startswith("Die Speicher des Proxmox-Hosts")
# Ein lokaler Speicher, der nur als „geteilt“ eingetragen ist (Netzfreigabe), zählt nicht als lokal.
geteilt = [{**SPEICHER[0], "shared": 1}] + SPEICHER[1:]
assert a.sicherungsspeicher(geteilt)[0] is None
def test_speicher_wunsch_aus_der_konfiguration():
a = _modul()
assert a.sicherungsspeicher(SPEICHER, "local")[0]["storage"] == "local"
s, fehlt = a.sicherungsspeicher(SPEICHER, "pbs-qnap")
assert s is None and "nie" in fehlt and "selbst sichern" in fehlt
assert "keine Sicherungen" in a.sicherungsspeicher(SPEICHER, "local-lvm")[1]
assert "gibt es nicht" in a.sicherungsspeicher(SPEICHER, "usb")[1]
aus = [{**SPEICHER[0], "active": 0}]
assert "nicht verfügbar" in a.sicherungsspeicher(aus, "local")[1]
def test_platzbedarf_ohne_bind_mount():
a = _modul()
# Läuft der Gast: die belegte rootfs. Der Bind-Mount (Datenspeicher des PBS) zählt nicht.
assert a.sicherung_bedarf(KONFIG_105, 3 * GIB) == 3 * GIB
# Läuft er nicht: die volle Größe der rootfs.
assert a.sicherung_bedarf(KONFIG_105, None) == 10 * GIB
# Weitere Volumes zählen nur mit backup=1.
mit = {**KONFIG_105, "mp1": "local-lvm:vm-105-disk-1,mp=/daten,backup=1,size=8G",
"mp2": "local-lvm:vm-105-disk-2,mp=/tmp2,size=4G"}
assert a.sicherung_bedarf(mit, 3 * GIB) == 11 * GIB
assert a.sicherung_bedarf({}, None) is None
def test_platzpruefung_frei_groesser_als_belegt_mal_1_2():
a = _modul()
stand = a.sicherung_stand(SPEICHER)
assert stand == {"speicher": "local", "art": "dir", "frei": 46835092 * 1024}
assert a.sicherung_pruefen(stand, 3 * GIB) == (True, None)
eng = {"speicher": "local", "frei": int(3.5 * GIB)}
ok, grund = a.sicherung_pruefen(eng, 3 * GIB) # 3,5 GB frei < 3 GB × 1,2
assert not ok and grund == "Zu wenig Platz auf „local“: frei 3,5 GB, nötig mehr als 3,6 GB (belegt 3,0 GB × 1,2)."
assert a.sicherung_pruefen({"speicher": "local", "frei": int(3.7 * GIB)}, 3 * GIB)[0] is True
assert a.sicherung_pruefen({"speicher": None, "fehler": "Kein lokaler Speicher …"}, 3 * GIB) == (
False, "Kein lokaler Speicher …")
def test_modus_snapshot_wo_die_rootfs_es_kann():
a = _modul()
arten = {s["storage"]: s["type"] for s in SPEICHER}
assert a.sicherung_modus(KONFIG_105, arten) == "snapshot"
assert a.sicherung_modus({"rootfs": "local:105/vm-105-disk-0.raw,size=8G"}, arten) == "stop"
def test_nur_eigene_sicherungen_zaehlen():
a = _modul()
eintraege = [
{"volid": "local:backup/vzdump-lxc-105-2026_09_24-22_00_00.tar.zst", "ctime": 20, "notes": NOTIZ},
{"volid": ALT, "ctime": 10, "notes": NOTIZ},
{"volid": "local:backup/vzdump-lxc-105-2026_09_01-03_00_00.tar.zst", "ctime": 5, "notes": "von Hand"},
{"volid": "local:backup/vzdump-lxc-104-2026_09_24-22_00_00.tar.zst", "ctime": 30, "notes": NOTIZ},
{"volid": "pbs-qnap:backup/ct/105/2026-09-24T20:00:00Z", "ctime": 40, "notes": NOTIZ},
]
assert [e["volid"] for e in a.eigene_sicherungen(eintraege, 105)] == [
ALT, "local:backup/vzdump-lxc-105-2026_09_24-22_00_00.tar.zst"]
assert len(a.eigene_sicherungen(eintraege)) == 3
assert a.eigene_sicherungen(None) == []
# --- Aktionen des Ausführers über Fakes -------------------------------------------------------
class Host:
"""Der Proxmox-Host als Fake: beantwortet pvesh und führt „Befehle“ nur ins Protokoll."""
def __init__(self, a, speicher=SPEICHER, belegt=3 * GIB):
self.befehle: list[list[str]] = []
self.inhalt = [{"volid": ALT, "vmid": 105, "ctime": 1790000000, "notes": NOTIZ},
{"volid": "local:backup/vzdump-lxc-105-2026_09_01-03_00_00.tar.zst", "vmid": 105,
"ctime": 1788000000, "notes": "von Hand"}]
self.status = "running"
self.antworten = {
"/cluster/resources": [{"vmid": 105, "type": "lxc", "tags": "backup;community-script"},
{"vmid": 106, "type": "qemu", "tags": ""}],
"/nodes/pve/storage": speicher,
"/nodes/pve/lxc/105/config": KONFIG_105,
"/nodes/pve/lxc/105/status/current": lambda: {"status": self.status, "disk": belegt},
"/nodes/pve/storage/local/content": lambda: self.inhalt,
}
a.NODE = "pve"
a._pvesh = self.pvesh
a._laufen = self.laufen
def pvesh(self, pfad, *parameter):
if pfad not in self.antworten:
raise RuntimeError(f"pvesh {pfad}: gibt es im Fake nicht")
wert = self.antworten[pfad]
return wert() if callable(wert) else wert
def laufen(self, befehl, zeitlimit=60, sanft=False):
self.befehle.append(list(befehl))
if befehl[0] == "vzdump":
self.sanft_vzdump = sanft
self.inhalt.append({"volid": "local:backup/vzdump-lxc-105-2026_09_24-22_10_00.tar.zst", "vmid": 105,
"ctime": int(time.time()), "notes": NOTIZ})
return 0, "INFO: Backup job finished successfully"
if befehl[:2] == ["pct", "shutdown"]:
self.status = "stopped"
return 0, "ok"
def test_sichern_vzdump_auf_local_mit_notiz(monkeypatch):
a = _modul()
host = Host(a)
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 0
assert antwort["text"].endswith("\nsicherung=local:backup/vzdump-lxc-105-2026_09_24-22_10_00.tar.zst")
befehl = host.befehle[-1]
assert befehl[:2] == ["vzdump", "105"] and host.sanft_vzdump is True
optionen = dict(zip(befehl[2::2], befehl[3::2], strict=True))
assert optionen["--storage"] == "local" and optionen["--mode"] == "snapshot"
assert optionen["--remove"] == "0" and optionen["--compress"] == "zstd"
assert optionen["--notes-template"].startswith("mc2-sicherung")
def test_sichern_lehnt_ab_ohne_speicher_und_ohne_platz(monkeypatch):
a = _modul()
host = Host(a, speicher=[s for s in SPEICHER if s["storage"] != "local"])
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 2 and "Kein lokaler Speicher" in antwort["text"] and host.befehle == []
host = Host(a, speicher=[{**SPEICHER[0], "avail": 2 * GIB}] + SPEICHER[1:], belegt=3 * GIB)
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 2 and "Zu wenig Platz" in antwort["text"] and host.befehle == []
# Wunsch in der Konfiguration zeigt auf den PBS: nie.
host = Host(a)
monkeypatch.setattr(a, "SICHERUNG_SPEICHER", "pbs-qnap")
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 2 and "Proxmox-Backup-Server" in antwort["text"] and host.befehle == []
# Ein Gast ohne erlaubtes Etikett bleibt unangetastet.
assert "kein erlaubtes Etikett" in a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 106}})["text"]
def test_sichern_ohne_kennzeichnung_ist_rot():
a = _modul()
host = Host(a)
def ohne_notiz(befehl, zeitlimit=60, sanft=False):
host.befehle.append(befehl)
host.inhalt.append({"volid": "local:backup/vzdump-lxc-105-2026_09_24-22_10_00.tar.zst", "vmid": 105,
"ctime": int(time.time()), "notes": ""})
return 0, "INFO: fertig"
a._laufen = ohne_notiz
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 1 and "mc2-sicherung" in antwort["text"]
def test_sicherung_zurueck_stoppt_spielt_zurueck_und_startet():
a = _modul()
host = Host(a)
antwort = a.ausfuehren({"aktion": "sicherung_zurueck", "parameter": {"vmid": 105, "sicherung": ALT}})
assert antwort["code"] == 0
assert host.befehle == [
["pct", "shutdown", "105", "--timeout", "120", "--forceStop", "1"],
["pct", "restore", "105", ALT, "--force", "1", "--storage", "local-lvm"], # der bisherige rootfs-Speicher
["pct", "start", "105"]]
def test_zurueckgespielt_und_geloescht_wird_nur_eigenes():
a = _modul()
host = Host(a)
fremd = "local:backup/vzdump-lxc-105-2026_09_01-03_00_00.tar.zst" # von Hand, ohne mc2-Notiz
for aktion in ("sicherung_zurueck", "sicherung_loeschen"):
for volid in (fremd, "local:backup/vzdump-lxc-104-2026_09_20-03_00_00.tar.zst",
"pbs-qnap:backup/ct/105/2026-09-24T20:00:00Z", "/var/lib/vz/dump/x.tar.zst; reboot", None):
antwort = a.ausfuehren({"aktion": aktion, "parameter": {"vmid": 105, "sicherung": volid}})
assert antwort["code"] == 2, (aktion, volid)
assert host.befehle == []
assert a.ausfuehren({"aktion": "sicherung_loeschen", "parameter": {"vmid": 105, "sicherung": ALT}})["code"] == 0
assert host.befehle == [["pvesm", "free", ALT]]
def test_laufen_mit_zeitlimit_und_sanftem_abbruch():
import sys
a = _modul()
assert a._laufen([sys.executable, "-c", "print('hallo')"]) == (0, "hallo")
beginn = time.time()
code, text = a._laufen([sys.executable, "-c", "import time; time.sleep(30)"], zeitlimit=1, sanft=True)
assert (code, text) == (124, "Zeitlimit (1 s) überschritten") and time.time() - beginn < 20
assert a._laufen(["gibt-es-nicht-mc2"])[0] == 127
def test_unerwarteter_fehler_wird_beantwortet():
"""Sonst bliebe der Auftrag offen, und der Homelab-Teil wartete bis zu seinem Zeitlimit."""
a = _modul()
Host(a)
a.AKTIONEN["sichern"] = lambda p: {}["kaputt"]
antwort = a.ausfuehren({"aktion": "sichern", "parameter": {"vmid": 105}})
assert antwort["code"] == 1 and "KeyError" in antwort["text"]
def test_bericht_sagt_ob_eine_sicherung_geht():
a = _modul()
host = Host(a)
host.antworten.update({
"/cluster/resources": [{"vmid": 105, "type": "lxc", "node": "pve", "name": "proxmox-backup-server",
"status": "running", "tags": "backup;community-script", "disk": 3 * GIB}],
"/nodes/pve/lxc/105/snapshot": [{"name": "current"}],
"/version": {"version": "9.2.11"},
"/nodes/pve/apt/update": [],
})
bericht = a.bericht()
assert bericht["host"]["sicherung"] == {"speicher": "local", "art": "dir", "frei": 46835092 * 1024}
assert bericht["host"]["nur_lesen"] is False # die Pflege legt sonst keine Aufträge an
pbs = bericht["gaeste"][0]
assert pbs["snapshot_moeglich"] is False and "Bind-Mount" in pbs["snapshot_grund"]
assert (pbs["sicherung_moeglich"], pbs["sicherung_grund"], pbs["sicherung_speicher"]) == (True, None, "local")
assert pbs["sicherungen"] == [ALT] # die fremde Sicherung von Hand zählt nicht
host.antworten["/nodes/pve/storage"] = SPEICHER[1:] # kein lokaler Speicher mit Sicherungen
pbs = a.bericht()["gaeste"][0]
assert pbs["sicherung_moeglich"] is False and "Kein lokaler Speicher" in pbs["sicherung_grund"]
# --- Der Ablauf im Homelab-Teil ---------------------------------------------------------------
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
monkeypatch.delenv("MC_AUSFUEHRER_TOKEN", raising=False)
einstellungen_mod.einstellungen.cache_clear()
monkeypatch.setattr(kanal, "_kontakt", {"zuletzt": None})
monkeypatch.setattr(inventar, "neueste_version", lambda repo: None)
monkeypatch.setattr(inventar, "erreichbar", lambda url: True)
monkeypatch.setattr(inventar.karenz, "skript_geaendert", lambda kennung: 0.0)
yield tmp_path
einstellungen_mod.einstellungen.cache_clear()
def _mit_sicherung(bericht: dict, **pbs) -> dict:
neu = json.loads(json.dumps(bericht))
gast = next(g for g in neu["gaeste"] if g["vmid"] == 105)
gast.update(pbs)
gast["os_updates"]["listen_stand"] = int(time.time()) # frische Listen: 58 Pakete bleiben „neu“
return neu
BERICHT_PBS = _mit_sicherung(BERICHT, sicherung_moeglich=True, sicherung_grund=None, sicherung_speicher="local",
sicherungen=[ALT])
NEU = "local:backup/vzdump-lxc-105-2026_09_24-22_10_00.tar.zst"
def _ausfuehrer(stopp: threading.Event, protokoll: list, fehlschlag: dict) -> None:
"""Nachgespielter Ausführer; fehlschlag: Aktion → (Code, Text)."""
while not stopp.is_set():
auftrag = kanal.naechster()
if not auftrag:
time.sleep(0.02)
continue
protokoll.append((auftrag["aktion"], auftrag["parameter"]))
code, text = fehlschlag.get(auftrag["aktion"], (0, ""))
if auftrag["aktion"] == "sichern" and code == 0:
text = f"INFO: fertig\nsicherung={NEU}"
elif auftrag["aktion"] == "bericht":
text = json.dumps(_mit_sicherung(BERICHT_PBS, sicherungen=[ALT, NEU]))
kanal.ergebnis(auftrag["id"], code, text)
@pytest.fixture
def lauf(daten, monkeypatch):
monkeypatch.setattr(updates, "WARTEN_NACH_UPDATE_S", 0)
echt = kanal.warten
monkeypatch.setattr(kanal, "warten", lambda aid, zeitlimit_s, takt_s=2.0: echt(aid, min(zeitlimit_s, 10), 0.02))
meldungen: list[tuple[str, bool]] = []
monkeypatch.setattr(updates, "_melden", lambda text, dringend=False: meldungen.append((text, dringend)))
monkeypatch.setattr(updates.update_verlauf, "eintragen", lambda *a: None)
kanal.bericht_speichern(BERICHT_PBS)
stopp, protokoll, fehlschlag = threading.Event(), [], {}
def starten(ziel: str, baustein: str) -> dict:
threading.Thread(target=_ausfuehrer, args=(stopp, protokoll, fehlschlag), daemon=True).start()
antwort = updates.starten(ziel, baustein)
assert antwort["ok"], antwort
ende = time.time() + 20
while time.time() < ende:
x = next((x for x in updates.laeufe() if x["id"] == antwort["lauf"]), {})
if x.get("status") == "fertig":
return x
time.sleep(0.05)
raise AssertionError("Lauf wurde nicht fertig")
yield starten, protokoll, meldungen, fehlschlag
stopp.set()
def test_pbs_gruen_mit_sicherung_und_alte_sicherung_weg(lauf):
starten, protokoll, meldungen, _ = lauf
x = starten("ct-105", "os")
assert x["ergebnis"] == "eingespielt"
assert protokoll == [("sichern", {"vmid": 105}), ("os_update", {"vmid": 105}), ("bericht", {}),
("sicherung_loeschen", {"vmid": 105, "sicherung": ALT})] # die neue bleibt
assert meldungen == [("Proxmox Backup Server: eingespielt, Prüfung grün.", False)]
def test_pbs_rot_spielt_die_sicherung_zurueck(lauf, monkeypatch):
starten, protokoll, meldungen, _ = lauf
monkeypatch.setattr(inventar, "erreichbar", lambda url: False)
x = starten("ct-105", "os")
assert x["ergebnis"] == "zurueckgerollt"
assert [p[0] for p in protokoll] == ["sichern", "os_update", "bericht", "sicherung_zurueck"]
assert protokoll[-1][1] == {"vmid": 105, "sicherung": NEU}
text, dringend = meldungen[0]
assert dringend and "Sicherung von vorher zurückgespielt (vzdump-lxc-105-2026_09_24-22_10_00.tar.zst)" in text
def test_pbs_rot_und_auch_das_zurueckspielen_scheitert(lauf, monkeypatch):
starten, _, meldungen, fehlschlag = lauf
monkeypatch.setattr(inventar, "erreichbar", lambda url: False)
fehlschlag["sicherung_zurueck"] = (1, "restore failed")
x = starten("ct-105", "os")
assert x["ergebnis"] == "fehler"
text, dringend = meldungen[0]
assert dringend and "Auch der Rückweg scheiterte" in text and f"Die Sicherung {NEU} liegt weiter" in text
def test_ohne_sicherung_beginnt_das_update_nicht(lauf):
starten, protokoll, meldungen, fehlschlag = lauf
fehlschlag["sichern"] = (2, "Abgelehnt: Zu wenig Platz auf „local“: frei 2,0 GB, nötig mehr als 3,6 GB.")
x = starten("ct-105", "os")
assert x["ergebnis"] == "fehler" and [p[0] for p in protokoll] == ["sichern"]
text, dringend = meldungen[0]
assert not dringend and "Update nicht begonnen" in text and "Zu wenig Platz" in text
assert "nichts geändert" in text and ".." not in text
def test_gescheiterter_snapshot_heisst_update_nicht_begonnen(lauf, monkeypatch):
"""Vorher hieß das „Update gescheitert … kein Rückweg“ (dringend), obwohl das Update nie lief."""
starten, protokoll, meldungen, fehlschlag = lauf
kanal.bericht_speichern(BERICHT)
monkeypatch.setattr(inventar, "neueste_version", lambda repo: {
"tag": "v1.27.3", "tag_roh": "v1.27.3", "version": "1.27.3", "datum": "2026-09-20"})
fehlschlag["snapshot"] = (1, "snapshot feature is not available")
x = starten("ct-104", "app")
assert x["ergebnis"] == "fehler" and [p[0] for p in protokoll] == ["snapshot"]
erwartet = ("Gitea: Update nicht begonnen — snapshot gescheitert: snapshot feature is not available. "
"Am Gerät wurde nichts geändert.")
assert meldungen == [(erwartet, False)]
def test_weder_snapshot_noch_sicherung_wie_bisher(lauf, monkeypatch):
starten, protokoll, meldungen, _ = lauf
kanal.bericht_speichern(_mit_sicherung(BERICHT, sicherung_moeglich=False,
sicherung_grund="Kein lokaler Speicher des Proxmox-Hosts …"))
monkeypatch.setattr(inventar, "erreichbar", lambda url: False)
x = starten("ct-105", "os")
assert [p[0] for p in protokoll] == ["os_update", "bericht"] and x["ergebnis"] == "fehler"
assert "weder Snapshot noch Sicherung" in meldungen[0][0]
# --- Was die Übersicht dazu sagt ----------------------------------------------------------------
def _pbs(bericht: dict) -> dict:
kanal.bericht_speichern(bericht)
return {z["id"]: z for z in inventar.ziele()["ziele"]}["ct-105"]
def test_rueckweg_und_rueckfrage_mit_sicherung(daten):
pbs = _pbs(BERICHT_PBS)
assert pbs["rueckweg"].startswith("Sicherung auf „local“ vor jedem Update (Bind-Mount mp0")
os_baustein = {b["id"]: b for b in pbs["bausteine"]}["os"]
assert os_baustein["aktion"]["frage"] == ("58 Pakete im Gast jetzt einspielen? Vorher wird eine Sicherung "
"angelegt; ist die Prüfung danach rot, wird sie zurückgespielt.")
def test_ohne_sicherung_sagt_die_rueckfrage_warum(daten):
pbs = _pbs(_mit_sicherung(BERICHT, sicherung_moeglich=False,
sicherung_grund="Zu wenig Platz auf „local“: frei 2,0 GB."))
assert "Auch keine Sicherung: Zu wenig Platz" in pbs["rueckweg"]
frage = {b["id"]: b for b in pbs["bausteine"]}["os"]["aktion"]["frage"]
assert "keinen automatischen Rückweg" in frage and "Eine Sicherung geht auch nicht: Zu wenig Platz" in frage
# Der alte Bericht (Ausführer noch ohne Sicherungen) bleibt wie bisher.
assert _pbs(BERICHT)["rueckweg"] == "Bind-Mount mp0 (/mnt/qnap-backup/pbs-datastore) — nur Backup möglich"
def test_waechter_laesst_gaeste_im_update_in_ruhe(daten):
from services import waechter
kanal.bericht_speichern(BERICHT_PBS)
updates._merken({"id": "x1", "ziel": "ct-105", "baustein": "os", "name": "PBS", "status": "laeuft",
"start": time.time(), "ende": None, "ergebnis": None, "text": None, "schritte": []})
ids = [e["id"] for e in inventar.erreichbarkeit()]
assert "ct-105" not in ids and "ct-104" in ids
assert all(b.id != "gast:ct-105" for b in waechter.pruefe_gaeste())