munin/scripts/sicherung-pruefen.sh

188 lines
8.2 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Probe einer Sicherung: zurückspielen, Munin darauf starten, prüfen,
# aufräumen. Eine Sicherung, die nie zurückgespielt wurde, ist nur eine
# Hoffnung (BETRIEB.md, "Regelmäßig").
#
# Geprüft wird:
# 1. Jede Datei der Sicherung lässt sich lesen bzw. zurückspielen.
# 2. Munin startet auf dem zurückgespielten Stand und meldet /ready.
# 3. Munin sieht so viele Workspaces, wie die Sicherung enthält.
#
# Der echte Server wird dabei nicht berührt: Die Probe läuft in einem
# eigenen Verzeichnis (SQLite) bzw. einer eigenen Datenbank (PostgreSQL), mit
# eigenen Geheimnissen und auf einem eigenen Port.
#
# ── Aufruf ───────────────────────────────────────────────────────────────────
#
# scripts/sicherung-pruefen.sh <Sicherungsordner>
#
# Der Ordner ist einer der zeitgestempelten Ordner von scripts/backup.sh. Ohne
# Angabe wird der neueste unter BACKUP_DIR (Vorgabe "backups") genommen.
#
# SQLite (Standard): braucht sqlite3.
#
# PostgreSQL: DB_DRIVER=postgres und PROBE_DATABASE_URL — eine LEERE
# Datenbank nur für die Probe, die einmal angelegt wird und dann bleibt:
#
# CREATE DATABASE munin_probe OWNER munin TEMPLATE template0 LOCALE 'C' ENCODING 'UTF8';
#
# Das Skript spielt dort hinein und löscht danach alle Schemas munin_* wieder
# daraus. Enthält die Datenbank vorher schon welche, bricht es ab, statt sie
# zu überschreiben. psql und pg_restore lassen sich wie bei backup.sh über
# PSQL und PG_RESTORE auf einen Container umstellen.
#
# Munin selbst: MUNIN_BIN zeigt auf das Programm. Ohne Angabe wird es aus dem
# Repository gebaut (braucht Go). PROBE_PORT wählt den Port (Vorgabe 18080).
#
# Endet mit 0, wenn die Probe bestanden ist, sonst mit 1.
set -euo pipefail
treiber="$(echo "${DB_DRIVER:-sqlite}" | tr '[:upper:]' '[:lower:]')"
port="${PROBE_PORT:-18080}"
repo="$(cd "$(dirname "$0")/.." && pwd)"
fehler() {
echo "PROBE NICHT BESTANDEN: $*" >&2
exit 1
}
# ── Welche Sicherung? ────────────────────────────────────────────────────────
sicherung="${1:-}"
if [ -z "$sicherung" ]; then
sicherung="$(ls -1d "${BACKUP_DIR:-backups}"/*/ 2>/dev/null | sort | tail -n 1 || true)"
[ -n "$sicherung" ] || fehler "keine Sicherung unter ${BACKUP_DIR:-backups} gefunden"
fi
sicherung="${sicherung%/}"
[ -d "$sicherung" ] || fehler "$sicherung ist kein Verzeichnis"
echo "Sicherung: $sicherung"
# ── Arbeitsverzeichnis und Aufräumen ─────────────────────────────────────────
arbeit="$(mktemp -d)"
munin_pid=""
aufraeumen() {
if [ -n "$munin_pid" ]; then
kill "$munin_pid" 2>/dev/null || true
wait "$munin_pid" 2>/dev/null || true
fi
if [ "$treiber" = "postgres" ] && [ -n "${probe_bereit:-}" ]; then
schemas_loeschen || echo "Warnung: Schemas in der Probe-Datenbank konnten nicht gelöscht werden." >&2
fi
rm -rf "$arbeit"
}
trap aufraeumen EXIT
# ── Munin besorgen ───────────────────────────────────────────────────────────
munin="${MUNIN_BIN:-}"
if [ -z "$munin" ]; then
command -v go >/dev/null 2>&1 || fehler "MUNIN_BIN ist nicht gesetzt und Go ist nicht installiert"
echo "Baue Munin …"
(cd "$repo" && CGO_ENABLED=0 go build -o "$arbeit/munin" .)
munin="$arbeit/munin"
fi
[ -x "$munin" ] || fehler "$munin ist nicht ausführbar"
# ── Zurückspielen ────────────────────────────────────────────────────────────
if [ "$treiber" = "sqlite" ]; then
command -v sqlite3 >/dev/null 2>&1 || fehler "sqlite3 ist nicht installiert"
[ -f "$sicherung/control.db" ] || fehler "control.db fehlt in der Sicherung"
mkdir -p "$arbeit/daten/workspaces"
cp "$sicherung/control.db" "$arbeit/daten/"
erwartet=0
for datei in "$sicherung"/workspaces/*.db; do
[ -e "$datei" ] || continue
cp "$datei" "$arbeit/daten/workspaces/"
erwartet=$((erwartet + 1))
done
for datei in "$arbeit/daten/control.db" "$arbeit"/daten/workspaces/*.db; do
[ -e "$datei" ] || continue
# Bei schwerem Schaden bricht sqlite3 selbst ab; die Meldung zählt dann
# als Ergebnis, statt dass set -e das Skript wortlos beendet.
ergebnis="$(sqlite3 "$datei" 'PRAGMA integrity_check;' 2>&1)" || true
[ "$ergebnis" = "ok" ] || fehler "$(basename "$datei"): $ergebnis"
done
echo "Alle Dateien unbeschädigt (control.db und $erwartet Workspace(s))"
umgebung=(DB_DRIVER=sqlite "DATA_DIR=$arbeit/daten")
elif [ "$treiber" = "postgres" ]; then
[ -n "${PROBE_DATABASE_URL:-}" ] || fehler "PROBE_DATABASE_URL ist nicht gesetzt (eine leere Datenbank nur für die Probe, siehe Kopf des Skripts)"
[ -f "$sicherung/control.dump" ] || fehler "control.dump fehlt in der Sicherung"
read -r -a psql <<<"${PSQL:-psql}"
read -r -a pg_restore <<<"${PG_RESTORE:-pg_restore}"
schemas_der_probe() {
"${psql[@]}" --dbname="$PROBE_DATABASE_URL" --no-align --tuples-only --command \
"SELECT nspname FROM pg_namespace WHERE nspname LIKE 'munin\_%' ORDER BY nspname" </dev/null
}
schemas_loeschen() {
for schema in $(schemas_der_probe); do
# client_min_messages: ohne die NOTICE-Zeile je gelöschter Tabelle.
"${psql[@]}" --dbname="$PROBE_DATABASE_URL" --quiet \
--command "SET client_min_messages TO warning; DROP SCHEMA \"$schema\" CASCADE" </dev/null
done
}
vorher="$(schemas_der_probe)" || fehler "Probe-Datenbank nicht erreichbar"
[ -z "$vorher" ] || fehler "Die Probe-Datenbank ist nicht leer (enthält $(echo $vorher)). Ist PROBE_DATABASE_URL wirklich die Probe-Datenbank und nicht die echte?"
probe_bereit=1
erwartet=0
for datei in "$sicherung/control.dump" "$sicherung"/workspaces/*.dump; do
[ -e "$datei" ] || continue
"${pg_restore[@]}" --dbname="$PROBE_DATABASE_URL" --no-owner --exit-on-error <"$datei" ||
fehler "$(basename "$datei") ließ sich nicht zurückspielen"
[ "$datei" = "$sicherung/control.dump" ] || erwartet=$((erwartet + 1))
done
echo "Alles zurückgespielt (control.dump und $erwartet Workspace(s))"
umgebung=(DB_DRIVER=postgres "DATABASE_URL=$PROBE_DATABASE_URL")
else
fehler "DB_DRIVER=$treiber ist unbekannt — erlaubt sind sqlite und postgres"
fi
# ── Munin starten und prüfen ─────────────────────────────────────────────────
#
# Eigene Geheimnisse: Die Probe braucht die echten nicht, und mit ihnen
# ließe sich nichts am echten Server anfangen.
# od liest genau 32 Bytes. (Nicht "tr … </dev/urandom | head": Dort bricht tr
# beim Schließen der Pipe ab, und mit pipefail endete das ganze Skript.)
geheimnis="$(od -An -tx1 -N32 /dev/urandom | tr -d ' \n')"
env "${umgebung[@]}" PORT="$port" SESSION_SECRET="$geheimnis" ADMIN_TOKEN="probe-${geheimnis:0:24}" \
EXPORT_DIR= "$munin" >"$arbeit/munin.log" 2>&1 &
munin_pid=$!
bereit=""
for _ in $(seq 1 60); do
if ! kill -0 "$munin_pid" 2>/dev/null; then
cat "$arbeit/munin.log" >&2
fehler "Munin hat sich beim Start beendet (Log oben)"
fi
if curl -fsS "http://127.0.0.1:$port/ready" >/dev/null 2>&1; then
bereit=1
break
fi
sleep 0.5
done
if [ -z "$bereit" ]; then
cat "$arbeit/munin.log" >&2
fehler "Munin meldet nach 30 Sekunden nicht /ready (Log oben)"
fi
# Munin nennt beim Start die Zahl der Workspaces: "Munin läuft auf … (N Workspaces)".
gesehen="$(sed -n 's/.*Munin läuft auf .* (\([0-9]*\) Workspaces).*/\1/p' "$arbeit/munin.log" | head -n 1)"
[ "$gesehen" = "$erwartet" ] ||
fehler "Munin sieht ${gesehen:-?} Workspace(s), die Sicherung enthält $erwartet"
echo "Munin startet, meldet /ready und sieht alle $erwartet Workspace(s)"
echo "PROBE BESTANDEN"