Files
AI-Profile-Router/platform/migration/restore-reference-backup.sh
T
2026-09-05 13:30:14 +02:00

180 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Restore persistent UI data and the approved secret subset from a reference
# host backup without reviving its obsolete containers or configuration files.
set -Eeuo pipefail
umask 077
BACKUP=${1:-}
STACK_DIR=${STACK_DIR:-/opt/mike-ai/stack}
SECRETS_DIR=${SECRETS_DIR:-/etc/mike-ai}
OPENWEBUI_VOLUME=${OPENWEBUI_VOLUME:-mike-ai_open-webui-data}
INSTALL_CONFIG=${INSTALL_CONFIG:-/root/mike-ai-install.env}
REFERENCE_OPENWEBUI_TAG=${REFERENCE_OPENWEBUI_TAG:-mike-ai/openwebui:reference}
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
log() { printf '\n==> %s\n' "$*"; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
[[ -n $BACKUP && -d $BACKUP ]] || \
die "Aufruf: $0 /pfad/zum/ki-migration-backup"
[[ -f $BACKUP/SHA256SUMS ]] || die "SHA256SUMS fehlt."
[[ -f $SECRETS_DIR/stack.env ]] || die "$SECRETS_DIR/stack.env fehlt."
ui_archive=$BACKUP/archives/open-webui-migrated.tar.gz
mcp_archive=$BACKUP/archives/mcp-and-search-config.tar.gz
image_archive=$BACKUP/images/openwebui-mcp-images.tar.gz
for archive in "$ui_archive" "$mcp_archive" "$image_archive"; do
[[ -f $archive ]] || die "Archiv fehlt: $archive"
expected=$(awk -v suffix="/${archive#"$BACKUP"/}" \
'$2 ~ suffix "$" {print $1; exit}' "$BACKUP/SHA256SUMS")
[[ -n $expected ]] || die "Keine Prüfsumme für $archive gefunden."
actual=$(sha256sum "$archive" | awk '{print $1}')
[[ $actual == "$expected" ]] || die "Prüfsumme falsch: $archive"
done
[[ -f $BACKUP/inventory/docker-images.txt ]] || \
die "Docker-Image-Inventar fehlt."
reference_openwebui_image=$(awk \
'$1 == "ghcr.io/open-webui/open-webui" { print $3; exit }' \
"$BACKUP/inventory/docker-images.txt")
[[ $reference_openwebui_image == sha256:* ]] || \
die "Gesicherte OpenWebUI-Image-ID ist nicht eindeutig ermittelbar."
[[ -f $BACKUP/inventory/docker-inspect.json ]] || \
die "Docker-Inspect-Inventar fehlt."
reference_openwebui_ref=$(python3 - "$BACKUP/inventory/docker-inspect.json" <<'PY'
import json
import sys
for container in json.load(open(sys.argv[1], encoding="utf-8")):
name = (container.get("Name") or "").lstrip("/").lower()
if name in {"open-webui", "mike-ai-open-webui"}:
image = (container.get("Config") or {}).get("Image", "")
if "@sha256:" in image:
print(image)
break
PY
)
reference_openwebui_revision=$(python3 - "$BACKUP/inventory/docker-inspect.json" <<'PY'
import json
import sys
for container in json.load(open(sys.argv[1], encoding="utf-8")):
name = (container.get("Name") or "").lstrip("/").lower()
if name in {"open-webui", "mike-ai-open-webui"}:
labels = (container.get("Config") or {}).get("Labels") or {}
print(labels.get("org.opencontainers.image.revision", ""))
break
PY
)
log "Versionsgleiches OpenWebUI-Image aus der Sicherung laden"
docker load -i "$image_archive" >/dev/null
selected_openwebui_image=$reference_openwebui_image
if ! docker image inspect "$selected_openwebui_image" >/dev/null 2>&1; then
[[ $reference_openwebui_ref == *@sha256:* ]] || \
die "Image-Archiv enthält OpenWebUI nicht und Registry-Digest fehlt."
log "Image-Archiv unvollständig; exakten OpenWebUI-Digest nachladen"
docker pull "$reference_openwebui_ref" >/dev/null
selected_openwebui_image=$reference_openwebui_ref
fi
docker image inspect "$selected_openwebui_image" >/dev/null 2>&1 || \
die "Gesichertes OpenWebUI-Image wurde nicht geladen."
actual_openwebui_revision=$(docker image inspect --format \
'{{ index .Config.Labels "org.opencontainers.image.revision" }}' \
"$selected_openwebui_image")
if [[ -n $reference_openwebui_revision && \
$actual_openwebui_revision != "$reference_openwebui_revision" ]]; then
die "OpenWebUI-Build-Revision stimmt nicht mit der Sicherung überein."
fi
docker tag "$selected_openwebui_image" "$REFERENCE_OPENWEBUI_TAG"
stage=$(mktemp -d /data/mike-ai-restore.XXXXXX)
trap 'rm -rf "$stage"' EXIT
log "OpenWebUI-Sicherung entpacken und prüfen"
tar -xzf "$ui_archive" -C "$stage"
ui_source=$stage/open-webui-migrated
[[ -s $ui_source/data/webui.db ]] || die "OpenWebUI-Datenbank fehlt oder ist leer."
[[ -f $ui_source/openwebui.env ]] || die "OpenWebUI-Umgebung fehlt."
old_webui_key=$(sed -n 's/^WEBUI_SECRET_KEY=//p' \
"$ui_source/openwebui.env" | head -n 1)
[[ -n $old_webui_key ]] || die "WEBUI_SECRET_KEY fehlt in der Sicherung."
log "Freigegebene MCP-Secrets isoliert entpacken"
mkdir -p "$stage/approved"
tar -xzf "$mcp_archive" -C "$stage/approved" \
etc/mike-ai/router-api-key \
etc/mike-ai/homeassistant-admin-mcp.env \
etc/mike-ai/arr-mcp.env \
etc/mike-ai/keys/unraid_root \
etc/mike-ai/ssh/known_hosts_unraid_ai
old_router_key=$(<"$stage/approved/etc/mike-ai/router-api-key")
[[ -n $old_router_key ]] || die "Router-Key in der Sicherung ist leer."
set_env_value() {
local file=$1 key=$2 value=$3 tmp
tmp=$(mktemp "${file}.XXXXXX")
awk -v key="$key" -v value="$value" '
BEGIN { done=0 }
$0 ~ "^" key "=" { print key "=" value; done=1; next }
{ print }
END { if (!done) print key "=" value }
' "$file" >"$tmp"
install -m 0600 "$tmp" "$file"
rm -f "$tmp"
}
log "Aktuellen leeren UI-Stand als Rückfall sichern"
volume_path=$(docker volume inspect -f '{{.Mountpoint}}' "$OPENWEBUI_VOLUME")
fallback=/data/open-webui-before-restore-$(date +%Y%m%d-%H%M%S).tar.gz
tar -czf "$fallback" -C "$volume_path" .
log "OpenWebUI-Daten atomar ersetzen"
docker stop mike-ai-open-webui >/dev/null 2>&1 || true
rsync -a --delete "$ui_source/data/" "$volume_path/"
log "Versionierte OpenWebUI-Filter anwenden"
filter_installer=$STACK_DIR/platform/openwebui/install-filters.sh
[[ -x $filter_installer ]] || die "OpenWebUI-Filter-Installer fehlt."
"$filter_installer"
log "Schlüssel und aktuelle Tool-Secrets wiederherstellen"
set_env_value "$SECRETS_DIR/stack.env" WEBUI_SECRET_KEY "$old_webui_key"
set_env_value "$SECRETS_DIR/stack.env" ROUTER_API_KEY "$old_router_key"
set_env_value "$SECRETS_DIR/stack.env" OPENWEBUI_IMAGE "$REFERENCE_OPENWEBUI_TAG"
if [[ -f $INSTALL_CONFIG ]]; then
set_env_value "$INSTALL_CONFIG" OPENWEBUI_IMAGE "$REFERENCE_OPENWEBUI_TAG"
fi
install -d -m 0700 "$SECRETS_DIR/keys" "$SECRETS_DIR/ssh"
install -m 0600 "$stage/approved/etc/mike-ai/homeassistant-admin-mcp.env" \
"$SECRETS_DIR/homeassistant-admin-mcp.env"
install -m 0600 "$stage/approved/etc/mike-ai/arr-mcp.env" \
"$SECRETS_DIR/arr-mcp.env"
install -m 0600 "$stage/approved/etc/mike-ai/keys/unraid_root" \
"$SECRETS_DIR/keys/unraid_root"
install -m 0600 "$stage/approved/etc/mike-ai/ssh/known_hosts_unraid_ai" \
"$SECRETS_DIR/ssh/known_hosts_unraid_ai"
log "Tool-Container mit der neuen Stackdefinition aktivieren"
"$STACK_DIR/platform/mcp/install-tools.sh"
log "Router und OpenWebUI mit den wiederhergestellten Schlüsseln neu erstellen"
cd "$STACK_DIR"
docker compose --env-file "$SECRETS_DIR/stack.env" up -d --force-recreate \
qwen3-tts piper tts-gateway router open-webui
deadline=$((SECONDS + 180))
for container in mike-ai-qwen3-tts mike-ai-piper mike-ai-tts-gateway \
mike-ai-router mike-ai-open-webui; do
until [[ $(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
"$container" 2>/dev/null || true) == healthy ]]; do
(( SECONDS < deadline )) || die "$container wurde nicht rechtzeitig gesund."
sleep 2
done
done
log "Wiederherstellung abgeschlossen"
printf 'Rückfallsicherung des vorherigen UI-Stands: %s\n' "$fallback"