From 74684c7e6c66c54d679537d1ffd243ad379ca1a1ebe32fcab6f26fb42951acf5 Mon Sep 17 00:00:00 2001 From: wlfb Date: Mon, 20 Jul 2026 08:11:53 +0200 Subject: [PATCH] feat: Kalt-Backup-Skript finance_pod Co-Authored-By: Claude Fable 5 --- backup_finance_pod.sh | 135 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100755 backup_finance_pod.sh diff --git a/backup_finance_pod.sh b/backup_finance_pod.sh new file mode 100755 index 0000000..529829a --- /dev/null +++ b/backup_finance_pod.sh @@ -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)"