diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 56728fb..e106664 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -50,6 +50,7 @@ sudo ./platform/migration/restore-reference-backup.sh \ Das Skript prüft zuerst die Archiv-Hashes und übernimmt ausschließlich: - die persistente OpenWebUI-Datenbank samt Einstellungen und Uploads, +- das zur Datenbank passende, gesicherte OpenWebUI-Container-Image, - den dazugehörigen WebUI- und Router-Schlüssel, - die freigegebenen HA-, ARR- und Unraid-Secrets, - `runraid`, dessen privaten Schlüssel, Zertifikat und Known-Hosts-Datei. @@ -59,5 +60,18 @@ Experimentdienste oder `.before-*`-Altstände. Vor dem Ersetzen des frischen OpenWebUI-Volumes wird unter `/data/open-webui-before-restore-*.tar.gz` eine Rückfallsicherung erstellt. Secret-Werte werden nicht ausgegeben. +OpenWebUI-Datenbanken sind nicht beliebig vor- oder rückwärtskompatibel. Das +Restore-Skript lädt deshalb bewusst das im Backup inventarisierte Original- +Image, versieht es lokal mit dem Tag `mike-ai/openwebui:reference` und trägt +diesen Tag sowohl in `stack.env` als auch – falls vorhanden – in +`/root/mike-ai-install.env` ein. Dadurch bleibt auch ein späterer, idempotenter +Installerlauf versionsgleich. Ein Upgrade auf eine neuere OpenWebUI-Version +erfolgt erst danach kontrolliert und mit einer eigenen Datenbanksicherung. +Falls ein älteres Image-Archiv trotz seines Namens das OpenWebUI-Image nicht +enthält, verwendet das Skript ausschließlich den im Docker-Inventar gesicherten +unveränderlichen Registry-Digest. Es fällt niemals auf `latest` zurück. +Da lokale Docker-Image-IDs beim Wiederherstellen von einem Registry-Digest +abweichen können, verifiziert es zusätzlich die gesicherte OCI-Build-Revision. + Der alte Host bleibt bis zum bestandenen Abnahmetest unverändert und dient nur als Referenz. Es werden keine Caches oder unbekannten Altverzeichnisse kopiert. diff --git a/docs/RECOVERY_REQUIREMENTS.md b/docs/RECOVERY_REQUIREMENTS.md index 0bb42a5..9a2864b 100644 --- a/docs/RECOVERY_REQUIREMENTS.md +++ b/docs/RECOVERY_REQUIREMENTS.md @@ -185,6 +185,13 @@ experimentelle Altcontainer zurückzubringen. Der Leerhost-Probelauf wird auf Athena praktisch protokolliert und seine Korrekturen fließen direkt in den Installer zurück. +Ein Container-Backup gilt nur dann als vollständig, wenn nach `docker save` +nicht bloß das Archiv und seine Prüfsumme existieren: Ein isolierter +Probeimport muss außerdem jede erwartete Image-ID beziehungsweise den +unveränderlichen Registry-Digest und die OCI-Build-Revision bestätigen. Die +OpenWebUI-Datenbank wird immer zusammen mit genau diesem geprüften Image +gesichert und wiederhergestellt. + Jeder Schritt muss wiederholbar, einzeln prüfbar und bei Fehlern abbrechbar sein. Ein fehlgeschlagener Schritt darf keinen halb aktivierten Dienst hinterlassen. diff --git a/platform/migration/restore-reference-backup.sh b/platform/migration/restore-reference-backup.sh index 99820ba..ad1acb3 100755 --- a/platform/migration/restore-reference-backup.sh +++ b/platform/migration/restore-reference-backup.sh @@ -8,6 +8,8 @@ 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' "$*"; } @@ -20,7 +22,8 @@ log() { printf '\n==> %s\n' "$*"; } ui_archive=$BACKUP/archives/open-webui-migrated.tar.gz mcp_archive=$BACKUP/archives/mcp-and-search-config.tar.gz -for archive in "$ui_archive" "$mcp_archive"; do +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") @@ -29,6 +32,62 @@ for archive in "$ui_archive" "$mcp_archive"; do [[ $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 @@ -82,6 +141,10 @@ rsync -a --delete "$ui_source/data/" "$volume_path/" 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/runraid" "$SECRETS_DIR/keys" "$SECRETS_DIR/ssh" install -m 0600 "$stage/approved/etc/mike-ai/homeassistant-admin-mcp.env" \