188 lines
8.2 KiB
Bash
Executable file
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"
|