79 lines
2.9 KiB
Bash
Executable File
79 lines
2.9 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)
|
|
openwebui_was_running=false
|
|
cleanup() {
|
|
if $openwebui_was_running; then
|
|
docker start "$OPENWEBUI_CONTAINER" >/dev/null 2>&1 || \
|
|
printf 'WARNUNG: OpenWebUI konnte nach dem Backup nicht gestartet werden.\n' >&2
|
|
fi
|
|
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. A filesystem archive while the database is being
|
|
# written can be internally inconsistent even if tar itself succeeds. Keep the
|
|
# interruption short, but always restart the container through the EXIT trap.
|
|
if [[ $(docker inspect -f '{{.State.Running}}' "$OPENWEBUI_CONTAINER" 2>/dev/null || true) == true ]]; then
|
|
openwebui_was_running=true
|
|
docker stop --time 30 "$OPENWEBUI_CONTAINER" >/dev/null
|
|
fi
|
|
tar -C "$volume_path" -czf "$stage/payload/openwebui-data.tar.gz" .
|
|
if $openwebui_was_running; then
|
|
docker start "$OPENWEBUI_CONTAINER" >/dev/null
|
|
openwebui_was_running=false
|
|
fi
|
|
|
|
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"
|