#!/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 \ xtts piper tts-gateway router open-webui deadline=$((SECONDS + 180)) for container in mike-ai-xtts 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"