b26fb40cbc
Erstlauf fand echten Stolperstein: pxar-Restore extrahiert als User korrekt, scheitert aber am chown root-eigener Dateien (llamaswap) -> Fehl-Exit trotz intakter Daten. Restore-Aufrufe jetzt mit --ignore-ownership/--ignore-acls (uns interessiert der INHALT); llamaswap-Fallback prueft den Inhalt statt des Exit-Codes. Kern-Datei-Grep matcht nur noch Dateien, keine Verzeichnisse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
7.1 KiB
Bash
150 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
# Restore-Probe (monatlich, 17.07.2026): „Backups sind Hoffnung, Restores sind Wissen."
|
|
# Spielt ECHTE Wiederherstellungen durch, statt nur Sicherungs-Erfolge zu zählen:
|
|
# 1) PBS: jüngsten host/aibox-Snapshot ermitteln (Alter < 36 h!), daraus den
|
|
# Kanarienvogel (state/backup-canary.txt aus hermes.pxar, selektiv via --pattern —
|
|
# ~/.hermes ist ~8 GB, wir ziehen nur die eine Datei) nach /tmp restaurieren und
|
|
# den Inhalt EXAKT vergleichen. Erstlauf-Fallback (Canary noch in keinem
|
|
# Snapshot): das winzige llamaswap.pxar komplett restaurieren und config.yaml
|
|
# auf Substanz prüfen.
|
|
# 2) Tarball: jüngstes mc2-state-*.tar.gz (Alter < 36 h) listen und den Canary
|
|
# (bzw. beim Erstlauf eine Kern-Datei) herausziehen und prüfen.
|
|
# 3) Canary für künftige Läufe sicherstellen (fester Inhalt → exakter Vergleich).
|
|
# Läuft als Hermes-Cron --no-agent (stdout = Telegram): monatlicher Herzschlag, bei
|
|
# Problemen ⚠️-Alarmtext. Komplett ohne sudo (PBS-Env + Client liegen im User-Raum).
|
|
set -uo pipefail
|
|
|
|
PBS_ENV="$HOME/.config/pbs-backup/env"
|
|
PBS_BIN="$HOME/bin/proxmox-backup-client"
|
|
CANARY="$HOME/.hermes/state/backup-canary.txt"
|
|
CANARY_SOLL="MC2-Backup-Kanarienvogel v1 — dieser Inhalt ist konstant und wird nach dem Restore exakt verglichen."
|
|
TARBALL_DIR="/srv/models/mc2-backups"
|
|
MAX_ALTER_H=36
|
|
|
|
TMP="$(mktemp -d /tmp/restore-probe.XXXXXX)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
FEHLER=()
|
|
BEFUND=()
|
|
|
|
briefkasten() { # $1=Text (stiller Chronik-Spiegel)
|
|
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$(python3 - "$1" <<'PY'
|
|
import json, sys
|
|
print(json.dumps({"text": sys.argv[1], "subject": "Restore-Probe",
|
|
"source": "restore-probe", "priority": "silent"}))
|
|
PY
|
|
)" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# ---------- 0) Canary sicherstellen (für DIESEN Lauf evtl. noch nicht im Backup) ----------
|
|
if [ ! -f "$CANARY" ] || [ "$(cat "$CANARY" 2>/dev/null)" != "$CANARY_SOLL" ]; then
|
|
printf '%s' "$CANARY_SOLL" > "$CANARY"
|
|
fi
|
|
|
|
# ---------- 1) PBS-Restore-Probe ----------
|
|
if [ ! -f "$PBS_ENV" ] || [ ! -x "$PBS_BIN" ]; then
|
|
FEHLER+=("PBS-Probe unmöglich: Env ($PBS_ENV) oder Client ($PBS_BIN) fehlt")
|
|
else
|
|
set -a; source "$PBS_ENV"; set +a
|
|
SNAP_JSON="$("$PBS_BIN" snapshot list host/aibox --output-format json 2>/dev/null \
|
|
|| "$PBS_BIN" snapshots --output-format json 2>/dev/null || true)"
|
|
SNAP_INFO="$(printf '%s' "$SNAP_JSON" | python3 -c '
|
|
import json, sys, time
|
|
try:
|
|
snaps = json.load(sys.stdin)
|
|
except Exception:
|
|
print("KEINE"); raise SystemExit
|
|
snaps = [s for s in snaps if isinstance(s, dict)
|
|
and s.get("backup-type") == "host" and s.get("backup-id") == "aibox"]
|
|
if not snaps:
|
|
print("KEINE"); raise SystemExit
|
|
s = max(snaps, key=lambda x: x.get("backup-time") or 0)
|
|
t = s.get("backup-time") or 0
|
|
alter_h = (time.time() - t) / 3600
|
|
stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t))
|
|
print(f"host/aibox/{stamp}")
|
|
print(round(alter_h, 1))
|
|
' 2>/dev/null || echo KEINE)"
|
|
SNAP="$(printf '%s\n' "$SNAP_INFO" | sed -n 1p)"
|
|
ALTER_H="$(printf '%s\n' "$SNAP_INFO" | sed -n 2p)"
|
|
if [ "$SNAP" = "KEINE" ] || [ -z "$SNAP" ]; then
|
|
FEHLER+=("PBS: kein host/aibox-Snapshot auffindbar (Sicherung steht evtl. STILL)")
|
|
else
|
|
if python3 -c "import sys; sys.exit(0 if float('$ALTER_H') <= $MAX_ALTER_H else 1)" 2>/dev/null; then
|
|
BEFUND+=("PBS-Snapshot $SNAP ist ${ALTER_H} h alt (frisch)")
|
|
else
|
|
FEHLER+=("PBS: jüngster Snapshot ist ${ALTER_H} h alt (> ${MAX_ALTER_H} h) — Sicherung steht STILL")
|
|
fi
|
|
# Selektives Canary-Restore aus hermes.pxar
|
|
mkdir -p "$TMP/pbs"
|
|
# --ignore-ownership/-acls: als User kann chown auf fremde Eigentümer nicht
|
|
# gelingen (sudo-frei ist Absicht) — uns interessiert der INHALT, nicht das chown.
|
|
if "$PBS_BIN" restore "$SNAP" hermes.pxar "$TMP/pbs" \
|
|
--pattern "state/backup-canary.txt" \
|
|
--ignore-ownership true --ignore-acls true >/dev/null 2>&1 \
|
|
&& [ -f "$TMP/pbs/state/backup-canary.txt" ]; then
|
|
if [ "$(cat "$TMP/pbs/state/backup-canary.txt")" = "$CANARY_SOLL" ]; then
|
|
BEFUND+=("PBS-Restore: Canary aus hermes.pxar wiederhergestellt, Inhalt EXAKT korrekt ✅")
|
|
else
|
|
FEHLER+=("PBS-Restore: Canary wiederhergestellt, aber Inhalt WEICHT AB (Datenkorruption?)")
|
|
fi
|
|
else
|
|
# Erstlauf-Fallback: winziges llamaswap.pxar komplett
|
|
mkdir -p "$TMP/ls"
|
|
"$PBS_BIN" restore "$SNAP" llamaswap.pxar "$TMP/ls" \
|
|
--ignore-ownership true --ignore-acls true >/dev/null 2>&1 || true
|
|
if [ -s "$TMP/ls/config.yaml" ] && grep -q 'models:' "$TMP/ls/config.yaml"; then
|
|
BEFUND+=("PBS-Restore: llamaswap.pxar wiederhergestellt, config.yaml plausibel ✅ (Canary war noch nicht im Snapshot — ab dem nächsten dabei)")
|
|
else
|
|
FEHLER+=("PBS-Restore FEHLGESCHLAGEN: weder Canary noch llamaswap.pxar sauber wiederherstellbar")
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ---------- 2) Tarball-Probe ----------
|
|
TARBALL="$(ls -1t "$TARBALL_DIR"/mc2-state-*.tar.gz 2>/dev/null | head -1 || true)"
|
|
if [ -z "$TARBALL" ]; then
|
|
FEHLER+=("Tarball: keine mc2-state-Sicherung unter $TARBALL_DIR gefunden")
|
|
else
|
|
T_ALTER_H="$(python3 -c "import os, time; print(round((time.time()-os.path.getmtime('$TARBALL'))/3600, 1))")"
|
|
if python3 -c "import sys; sys.exit(0 if float('$T_ALTER_H') <= $MAX_ALTER_H else 1)"; then
|
|
BEFUND+=("Tarball $(basename "$TARBALL") ist ${T_ALTER_H} h alt (frisch)")
|
|
else
|
|
FEHLER+=("Tarball: jüngste Sicherung ist ${T_ALTER_H} h alt (> ${MAX_ALTER_H} h) — mc2-backup steht STILL")
|
|
fi
|
|
CANARY_PFAD="$(tar -tzf "$TARBALL" 2>/dev/null | grep 'backup-canary\.txt$' | head -1 || true)"
|
|
if [ -n "$CANARY_PFAD" ]; then
|
|
if [ "$(tar -xzf "$TARBALL" -O "$CANARY_PFAD" 2>/dev/null)" = "$CANARY_SOLL" ]; then
|
|
BEFUND+=("Tarball-Restore: Canary herausgezogen, Inhalt EXAKT korrekt ✅")
|
|
else
|
|
FEHLER+=("Tarball-Restore: Canary im Archiv, aber Inhalt WEICHT AB")
|
|
fi
|
|
else
|
|
KERN="$(tar -tzf "$TARBALL" 2>/dev/null | grep -E '(cron|state)/[^/]+$' | head -1 || true)"
|
|
if [ -n "$KERN" ] && [ -n "$(tar -xzf "$TARBALL" -O "$KERN" 2>/dev/null | head -c 1)" ]; then
|
|
BEFUND+=("Tarball-Restore: Kern-Datei ($KERN) herausgezogen, nicht leer ✅ (Canary ab der nächsten Sicherung dabei)")
|
|
else
|
|
FEHLER+=("Tarball-Restore FEHLGESCHLAGEN: Archiv listet keine Kern-Dateien oder Extraktion leer")
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ---------- Bericht (stdout = Telegram via --no-agent) ----------
|
|
if [ "${#FEHLER[@]}" -eq 0 ]; then
|
|
echo "Commander, monatliche Restore-Probe: beide Wiederherstellungs-Wege funktionieren wirklich."
|
|
printf '• %s\n' "${BEFUND[@]}"
|
|
briefkasten "Restore-Probe grün: $(printf '%s · ' "${BEFUND[@]}")"
|
|
else
|
|
echo "⚠️ Commander, die monatliche Restore-Probe hat ein ECHTES Problem gefunden:"
|
|
printf '• %s\n' "${FEHLER[@]}"
|
|
if [ "${#BEFUND[@]}" -gt 0 ]; then
|
|
echo "Was funktioniert hat:"
|
|
printf '• %s\n' "${BEFUND[@]}"
|
|
fi
|
|
echo "Bitte zeitnah draufschauen — ein Backup, das nicht zurückkommt, ist keins."
|
|
briefkasten "⚠️ Restore-Probe ROT: $(printf '%s · ' "${FEHLER[@]}")"
|
|
exit 1
|
|
fi
|