munin/scripts/backup.sh

192 lines
8.5 KiB
Bash
Executable file

#!/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
# <stempel>/control.db <stempel>/control.dump
# <stempel>/workspaces/<id>.db <stempel>/workspaces/munin_ws_<id>.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" </dev/null >"$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