#!/usr/bin/env bash # # Sichert die zentrale Kontroll-Datenbank und jeden Workspace einzeln — mit # SQLite (Standard) oder PostgreSQL, je nach DB_DRIVER wie beim Server. # # Jede Sicherung landet in einem eigenen, zeitgestempelten Ordner. Da jeder # Workspace für sich gesichert wird, lässt sich später auch nur eine einzelne # Abteilung gezielt wiederherstellen, ohne die übrigen anzufassen. # # SQLite PostgreSQL # /control.db /control.dump # /workspaces/.db /workspaces/munin_ws_.dump # # ── SQLite ─────────────────────────────────────────────────────────────────── # # sqlite3 .backup ist dafür gebaut, auch während der Server im WAL-Modus # schreibt (siehe AUSBAUSTUFEN.md 4.4 und README.md "Datensicherung") — eine # einfache Dateikopie wäre hier riskant. # # scripts/backup.sh [Datenverzeichnis] [Sicherungsverzeichnis] # # Beide Pfade lassen sich auch über die Umgebungsvariablen DATA_DIR und # BACKUP_DIR setzen (Vorgaben: "data" und "backups") — praktisch für einen # Cronjob, der ohnehin feste Pfade mitgibt. # # ── PostgreSQL ─────────────────────────────────────────────────────────────── # # DB_DRIVER=postgres DATABASE_URL=postgres://… BACKUP_DIR=… scripts/backup.sh # # pg_dump sichert jedes Schema (munin_control, munin_ws_…) in eine eigene # Datei im custom-Format, im laufenden Betrieb und ohne den Server zu # bremsen. Jede Datei enthält das Schema selbst samt dem Workspace-Namen als # Kommentar, lässt sich also mit pg_restore ohne Vorarbeit zurückspielen # (BETRIEB.md, "Aus einer Sicherung wiederherstellen"). # # Nicht verwenden: einen einzigen Dump aller Schemas. Aus dem lässt sich ein # einzelner Workspace nicht zurückholen — pg_restore --schema legt das Schema # selbst nicht an (am 2026-10-16 ausprobiert). # # Gebraucht werden psql, pg_dump und pg_restore in mindestens der Hauptversion # des Servers (pg_dump verweigert sonst die Arbeit). Fehlen sie auf dem # Sicherungsrechner, lassen sie sich aus dem PostgreSQL-Container leihen: # # PSQL="docker exec -i munin-postgres psql" \ # PG_DUMP="docker exec -i munin-postgres pg_dump" \ # PG_RESTORE="docker exec -i munin-postgres pg_restore" \ # DB_DRIVER=postgres DATABASE_URL=postgres://munin@localhost:5432/munin scripts/backup.sh # # Das Passwort besser nicht in DATABASE_URL schreiben — die Adresse steht als # Aufrufparameter in der Prozessliste. Stattdessen ~/.pgpass oder PGPASSWORD # (bei docker exec mit -e PGPASSWORD weiterreichen). # # ── Beispiel für Cron ──────────────────────────────────────────────────────── # # Täglich um 3 Uhr, mit 14 Tagen Aufbewahrung: # # 0 3 * * * DATA_DIR=/srv/munin/data BACKUP_DIR=/srv/backups RETENTION_DAYS=14 \ # /srv/munin/scripts/backup.sh >> /var/log/munin-backup.log 2>&1 # # Ohne RETENTION_DAYS bleibt jede Sicherung liegen — lieber zu viel als # versehentlich eine noch gebrauchte Sicherung verlieren. # # ── Wiederherstellen ───────────────────────────────────────────────────────── # # Die genauen Schritte für beide Datenbanken stehen in BETRIEB.md, Abschnitt # "Aus einer Sicherung wiederherstellen". Kurz für SQLite: Server stoppen, # -wal/-shm der alten Fassung entfernen, .db-Datei zurückkopieren, starten. set -euo pipefail treiber="$(echo "${DB_DRIVER:-sqlite}" | tr '[:upper:]' '[:lower:]')" datenverzeichnis="${1:-${DATA_DIR:-data}}" sicherungsverzeichnis="${2:-${BACKUP_DIR:-backups}}" stempel="$(date +%Y-%m-%dT%H-%M-%S)" ziel="$sicherungsverzeichnis/$stempel" # ── SQLite ─────────────────────────────────────────────────────────────────── sichere_sqlite() { if ! command -v sqlite3 >/dev/null 2>&1; then echo "sqlite3 ist nicht installiert — ohne dieses Werkzeug keine sichere Sicherung im laufenden Betrieb." >&2 exit 1 fi if [ ! -d "$datenverzeichnis" ]; then echo "Datenverzeichnis $datenverzeichnis existiert nicht." >&2 exit 1 fi mkdir -p "$ziel/workspaces" sichern() { sqlite3 "$1" ".backup '$2'" } if [ -f "$datenverzeichnis/control.db" ]; then sichern "$datenverzeichnis/control.db" "$ziel/control.db" echo "control.db gesichert" else echo "Warnung: $datenverzeichnis/control.db nicht gefunden — übersprungen." >&2 fi anzahl=0 for datei in "$datenverzeichnis"/workspaces/*.db; do [ -e "$datei" ] || continue # kein Workspace vorhanden (glob lief leer) sichern "$datei" "$ziel/workspaces/$(basename "$datei")" anzahl=$((anzahl + 1)) done echo "$anzahl Workspace-Datei(en) gesichert nach $ziel" } # ── PostgreSQL ─────────────────────────────────────────────────────────────── sichere_postgres() { if [ -z "${DATABASE_URL:-}" ]; then echo "DB_DRIVER=postgres, aber DATABASE_URL ist nicht gesetzt." >&2 exit 1 fi # Die Befehle dürfen aus mehreren Wörtern bestehen ("docker exec -i … pg_dump"), # deshalb als Liste eingelesen. read -r -a psql <<<"${PSQL:-psql}" read -r -a pg_dump <<<"${PG_DUMP:-pg_dump}" read -r -a pg_restore <<<"${PG_RESTORE:-pg_restore}" for befehl in "${psql[0]}" "${pg_dump[0]}" "${pg_restore[0]}"; do if ! command -v "$befehl" >/dev/null 2>&1; then echo "$befehl ist nicht installiert. Siehe Kopf dieses Skripts: PSQL, PG_DUMP und PG_RESTORE lassen sich auf einen Container umstellen." >&2 exit 1 fi done # Alle Schemas von Munin: die Kontroll-Datenbank und je Workspace eines. # Der Unterstrich muss im LIKE maskiert werden, sonst steht er für ein # beliebiges Zeichen. schemas="$("${psql[@]}" --dbname="$DATABASE_URL" --no-align --tuples-only --command \ "SELECT nspname FROM pg_namespace WHERE nspname = 'munin_control' OR nspname LIKE 'munin\_ws\_%' ORDER BY nspname")" if ! grep -qx 'munin_control' <<<"$schemas"; then echo "Schema munin_control nicht gefunden — ist DATABASE_URL die Datenbank von Munin?" >&2 exit 1 fi mkdir -p "$ziel/workspaces" # Bewusst "for" statt "while read": Mit "docker exec -i" liest pg_dump die # Standardeingabe — in einer while-read-Schleife also den Rest der Liste, # und nach dem ersten Schema wäre Schluss. Schemanamen enthalten keine # Leerzeichen, das Aufteilen an Zeilenumbrüchen ist deshalb sicher. anzahl=0 for schema in $schemas; do if [ "$schema" = "munin_control" ]; then datei="$ziel/control.dump" else datei="$ziel/workspaces/$schema.dump" fi # Ausgabe über die Standardausgabe statt --file, damit es auch mit # "docker exec" funktioniert: Die Datei entsteht dann auf dem Rechner, # der das Skript ausführt, nicht im Container. "${pg_dump[@]}" --dbname="$DATABASE_URL" --format=custom --schema="$schema" "$datei" # Gleich prüfen, ob sich die Datei lesen lässt. Eine Sicherung, die # beim Zurückspielen scheitert, fällt sonst erst im Ernstfall auf. if ! "${pg_restore[@]}" --list <"$datei" >/dev/null; then echo "Sicherung von $schema ist nicht lesbar: $datei" >&2 exit 1 fi if [ "$schema" = "munin_control" ]; then echo "munin_control gesichert" else anzahl=$((anzahl + 1)) fi done echo "$anzahl Workspace-Schema(s) gesichert nach $ziel" } case "$treiber" in sqlite) sichere_sqlite ;; postgres) sichere_postgres ;; *) echo "DB_DRIVER=$treiber ist unbekannt — erlaubt sind sqlite und postgres." >&2 exit 1 ;; esac # Alte Sicherungen aufräumen, falls eine Aufbewahrungsfrist gesetzt ist. if [ -n "${RETENTION_DAYS:-}" ]; then find "$sicherungsverzeichnis" -mindepth 1 -maxdepth 1 -type d -mtime "+$RETENTION_DAYS" -print -exec rm -rf {} \; fi