feat: Kalt-Backup-Skript finance_pod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 08:11:53 +02:00
parent 9316d72bb4
commit 74684c7e6c

135
backup_finance_pod.sh Executable file
View File

@@ -0,0 +1,135 @@
#!/bin/bash
# Kalt-Backup-Skript für den finance_pod (Postgres-Daten, Grafana-Daten,
# Uploads/Inbox und das Secrets-.env unter ~/.local/share/finance_pod).
# Stoppt den systemd-User-Service für die Dauer der Sicherung (kurze
# Downtime), erstellt ein tar.gz, startet den Service wieder, prüft die
# Erreichbarkeit und die Archiv-Integrität, und schützt das Archiv per
# chmod 600 (es enthält Secrets aus .env).
#
# Usage: ./backup_finance_pod.sh [ZIELDIR] (Default: $HOME/backups)
set -e
# Secrets landen im Archiv (.env) -> alle vom Skript neu erzeugten
# Dateien/Verzeichnisse sollen von Anfang an nur für den Besitzer lesbar
# sein, nicht erst nach einem nachträglichen chmod.
umask 077
POD_NAME='finance_pod'
SERVICE_NAME="pod-${POD_NAME}.service"
SOURCE_PARENT="$HOME/.local/share"
BIND_DIR="$SOURCE_PARENT/$POD_NAME"
TARGET_DIR="${1:-$HOME/backups}"
HOST_LOCAL_IP='127.0.0.1'
API_HOST_PORT='8096'
CHECK_URL="http://$HOST_LOCAL_IP:$API_HOST_PORT/login"
if [ ! -d "$BIND_DIR" ]; then
echo "ERROR: $BIND_DIR existiert nicht." >&2
exit 1
fi
# Zielverzeichnis anlegen, BEVOR der Service gestoppt wird: wenn das Ziel
# unbeschreibbar ist, bricht das Skript hier ab und der Service lief nie
# an, bleibt also ungestört aktiv. Nur beim NEU-Anlegen explizit auf 700
# setzen (ein bereits vorhandenes Zielverzeichnis wird in seinen
# bestehenden Rechten nicht angetastet).
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
chmod 700 "$TARGET_DIR"
else
mkdir -p "$TARGET_DIR"
fi
ARCHIVE="$TARGET_DIR/${POD_NAME}_$(date +%F_%H%M%S).tar.gz"
# Merkt sich, ob DIESES Skript den Service gestoppt hat. Der EXIT-Trap
# startet ihn bei jedem Fehler wieder - unabhängig davon, an welcher Stelle
# das Skript abbricht (set -e sorgt dafür, dass jeder Fehlschlag hierher
# springt). Zusätzlich entfernt der Trap ein evtl. bereits angelegtes,
# aber unvollständiges/fehlerhaftes Archiv - es kann Secrets (.env)
# enthalten und darf im Fehlerfall nicht liegen bleiben.
SERVICE_STOPPED=0
restart_on_error() {
rc=$?
if [ "$rc" -ne 0 ]; then
if [ -n "${ARCHIVE:-}" ] && [ -f "$ARCHIVE" ]; then
echo "Entferne unvollständiges/fehlerhaftes Archiv $ARCHIVE (kann Secrets enthalten)..." >&2
rm -f "$ARCHIVE"
fi
if [ "$SERVICE_STOPPED" -eq 1 ]; then
echo "FEHLER (Exit-Code $rc) — starte $SERVICE_NAME sicherheitshalber wieder..." >&2
systemctl --user start "$SERVICE_NAME" || true
fi
fi
exit "$rc"
}
trap restart_on_error EXIT
echo "Stoppe $SERVICE_NAME für das Kalt-Backup..."
systemctl --user stop "$SERVICE_NAME"
SERVICE_STOPPED=1
# tar läuft im rootless-Podman-User-Namespace (`podman unshare`): das
# Postgres-Datenverzeichnis gehört auf dem Host der subuid-verschobenen UID
# (Container-UID 999 -> Host-UID via /etc/subuid), für `wlfb` direkt daher
# unlesbar (drwx------). Im User-Namespace mappt UID 0 auf den aufrufenden
# Host-User zurück und darf als Namespace-root alle verschobenen Dateien
# lesen; die von ns-root neu erzeugte Archivdatei gehört auf dem Host
# dadurch bereits `wlfb` - KEIN nachträgliches chown noetig (und keins
# versuchen: `podman unshare chown "$(id -u):$(id -g)"` wertet die
# id-Substitution auf dem HOST aus, aber im Namespace ist genau diese
# host-UID/GID bereits subuid-verschoben, sodass der Aufruf die Datei auf
# eine fremde subuid-UID umbiegt und das anschliessende `chmod 600` mit
# EPERM scheitert). Zusätzlich vererbt `podman unshare` NICHT die umask
# des aufrufenden Skripts (dort dokumentiert 0022 statt der oben gesetzten
# 077) - daher `umask 077` explizit erneut innerhalb des unshare-Subshells,
# damit das Archiv von Geburt an 600 ist statt bis zum finalen chmod als
# 644 mit Secrets dazuliegen.
echo "Erstelle Archiv $ARCHIVE (im Podman-User-Namespace, wegen UID-verschobener Postgres-Daten)..."
podman unshare sh -c 'umask 077; tar czf "$1" -C "$2" "$3"' _ "$ARCHIVE" "$SOURCE_PARENT" "$POD_NAME"
echo "Starte $SERVICE_NAME wieder..."
systemctl --user start "$SERVICE_NAME"
SERVICE_STOPPED=0
echo "Warte auf Bereitschaft ($CHECK_URL)..."
READY=0
CODE=''
for attempt in $(seq 1 30); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$CHECK_URL" || true)
if [ "$CODE" = "200" ]; then
echo "API ist wieder erreichbar (200)."
READY=1
break
fi
sleep 2
done
if [ "$READY" -ne 1 ]; then
echo "ERROR: API wurde nach dem Neustart nicht rechtzeitig bereit (letzter Status: $CODE)." >&2
exit 1
fi
echo "Prüfe Archiv-Integrität (tar -tzf)..."
LISTING=$(tar -tzf "$ARCHIVE")
for required in ".env" "postgres-data" "grafana-data" "data"; do
# Regex-Sonderzeichen (nur '.' kommt in dieser Liste vor, bei ".env")
# escapen, damit z.B. "xenv" nicht faelschlich auf ".env" matcht.
escaped="${required//./\\.}"
if ! echo "$LISTING" | grep -Eq "^${POD_NAME}/${escaped}(/.*)?\$"; then
echo "ERROR: Archiv enthält kein '$required'." >&2
exit 1
fi
done
echo "Archiv-Integrität OK (.env, postgres-data, grafana-data, data vorhanden)."
# Archiv enthält Secrets (.env) -> nur der Besitzer darf lesen. (umask 077
# hat das Archiv bereits als 600 angelegt; chmod hier ist eine zusätzliche
# Absicherung, falls sich das je ändert.)
chmod 600 "$ARCHIVE"
SIZE=$(du -h "$ARCHIVE" | cut -f1)
echo "Backup fertig: $ARCHIVE ($SIZE, chmod 600)"