192 lines
8.5 KiB
Bash
Executable file
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
|