Files
AI-Profile-Router/platform/recovery/create-recovery-bundle.sh
T

95 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
OUTPUT=${1:-}
RECIPIENT_FILE=${AGE_RECIPIENT_FILE:-/etc/mike-ai/recovery.age-recipient}
OPENWEBUI_VOLUME=${OPENWEBUI_VOLUME:-mike-ai_open-webui-data}
OPENWEBUI_CONTAINER=${OPENWEBUI_CONTAINER:-mike-ai-open-webui}
STACK_DIR=${STACK_DIR:-/opt/mike-ai/stack}
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
[[ -n $OUTPUT ]] || die "Aufruf: $0 /sicheres/offhost-ziel/athena-recovery-YYYYMMDD.tar.age"
[[ -s $RECIPIENT_FILE ]] || die "Age-Empfängerdatei fehlt: $RECIPIENT_FILE"
command -v age >/dev/null || die "age ist nicht installiert."
command -v docker >/dev/null || die "Docker ist nicht installiert."
recipient=$(awk '/^age1[[:alnum:]]+$/ {print; exit}' "$RECIPIENT_FILE")
[[ -n $recipient ]] || die "Keine gültige öffentliche age-Adresse gefunden."
install -d -m 0700 "$(dirname "$OUTPUT")"
[[ ! -e $OUTPUT ]] || die "Zieldatei existiert bereits: $OUTPUT"
stage=$(mktemp -d /tmp/mike-ai-recovery.XXXXXX)
sqlite_snapshot=""
cleanup() {
[[ -z $sqlite_snapshot ]] || rm -f -- "$sqlite_snapshot"
rm -rf "$stage"
}
trap cleanup EXIT
mkdir -p "$stage/rootfs" "$stage/payload"
for source in \
/etc/mike-ai \
/root/mike-ai-install.env \
/usr/local/bin/runraid; do
[[ -e $source ]] || continue
rsync -aR "$source" "$stage/rootfs/"
done
tar -C "$stage/rootfs" -czf "$stage/payload/host-config.tar.gz" .
volume_path=$(docker volume inspect -f '{{.Mountpoint}}' "$OPENWEBUI_VOLUME")
[[ -s $volume_path/webui.db ]] || die "OpenWebUI-Datenbank fehlt oder ist leer."
# OpenWebUI uses SQLite. The online backup API creates a transactionally
# consistent snapshot while the service remains available. The archive omits
# the live DB/WAL/SHM and stores that snapshot under the canonical DB name.
[[ $(docker inspect -f '{{.State.Running}}' "$OPENWEBUI_CONTAINER" 2>/dev/null || true) == true ]] || \
die "OpenWebUI läuft nicht; Online-Datenbanksicherung nicht möglich."
sqlite_snapshot="$volume_path/.mike-ai-recovery-webui.db"
rm -f -- "$sqlite_snapshot"
docker exec -i "$OPENWEBUI_CONTAINER" python - <<'PY'
import os
import sqlite3
source = "/app/backend/data/webui.db"
snapshot = "/app/backend/data/.mike-ai-recovery-webui.db"
if os.path.exists(snapshot):
os.unlink(snapshot)
with sqlite3.connect(source) as src, sqlite3.connect(snapshot) as dst:
src.backup(dst)
with sqlite3.connect(snapshot) as check:
result = check.execute("PRAGMA integrity_check").fetchone()
if not result or result[0] != "ok":
raise SystemExit("SQLite integrity_check failed")
PY
[[ -s $sqlite_snapshot ]] || die "Konsistenter OpenWebUI-Snapshot wurde nicht erzeugt."
tar -C "$volume_path" \
--exclude='./webui.db' \
--exclude='./webui.db-wal' \
--exclude='./webui.db-shm' \
--transform='s#^\./\.mike-ai-recovery-webui\.db$#./webui.db#' \
-czf "$stage/payload/openwebui-data.tar.gz" .
tar -tzf "$stage/payload/openwebui-data.tar.gz" | grep -Eq '(^|/)webui\.db$' || \
die "OpenWebUI-Archiv enthält den konsistenten Datenbanksnapshot nicht."
source_commit=unknown
[[ ! -s $STACK_DIR/.mike-ai-source-commit ]] || source_commit=$(<"$STACK_DIR/.mike-ai-source-commit")
cat >"$stage/payload/METADATA" <<EOF
created_utc=$(date -u +%FT%TZ)
hostname=$(hostname)
source_commit=$source_commit
openwebui_volume=$OPENWEBUI_VOLUME
EOF
(
cd "$stage/payload"
sha256sum host-config.tar.gz openwebui-data.tar.gz METADATA >SHA256SUMS
tar -czf "$stage/bundle.tar.gz" \
host-config.tar.gz openwebui-data.tar.gz METADATA SHA256SUMS
)
age -r "$recipient" -o "$OUTPUT.partial" "$stage/bundle.tar.gz"
mv "$OUTPUT.partial" "$OUTPUT"
chmod 0600 "$OUTPUT"
printf 'RECOVERY_BUNDLE_OK %s\n' "$OUTPUT"