From 9685b616cf9fd78b44e288687ae9d523787959b8 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Aug 2026 11:34:11 +0000 Subject: [PATCH] Phase 13.5 Recovery Archive Hotfix: Fix A (Reihenfolge-Bug nach docker image save) + cleanup-partial-recovery-archive Command --- .../control-plane/cp2a2_2b/rq-historical | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 20f673f..278927e 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -1957,11 +1957,14 @@ archive_image() { # $1 = role asha="$(sha256sum "$base/image.tar" | awk '{print $1}')" asize="$(stat -c '%s' "$base/image.tar")" # SHAs der Metadaten (zur Laufzeit, aus Build-Session / Host-SoT) + # HINWEIS: APP_MANIFEST_SHA_CUR / RUNTIME_CONFIG_SHA_CUR werden hier NICHT + # berechnet โ€” app.manifest und runtime-config.txt existieren zu diesem Zeitpunkt + # noch nicht (werden erst unten erzeugt). sha256sum auf nicht-existente Datei + # wuerde unter set -euo pipefail mit EXIT=1 abbrechen (bewiesener Root Cause). + # Die korrekte Berechnung erfolgt NACH write_runtime_config/write_app_manifest. HOST_SOT_RUNNER_SHA_CUR="$(sha256sum /opt/historical-v2/app/run_backfill_year.py 2>/dev/null | awk '{print $1}')" BUILD_INPUT_SHA_CUR="$(build_context_manifest 2>/dev/null | sha256sum | awk '{print $1}')" WRAPPER_SHA_CUR="$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')" - APP_MANIFEST_SHA_CUR="$(sha256sum "$base/app.manifest" 2>/dev/null | awk '{print $1}')" - RUNTIME_CONFIG_SHA_CUR="$(sha256sum "$base/runtime-config.txt" 2>/dev/null | awk '{print $1}')" gen_archive_meta "$role" "$base" "$id" "$asize" "$asha" write_runtime_config "$role" "$base" write_app_manifest "$role" "$base" "$id" @@ -2040,6 +2043,81 @@ verify_archive_cmd() { # $1 = role (read-only) verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)" exit 0 } + +# ============================================================================= +# cleanup-partial-recovery-archive (Phase 13.5 ยท Recovery Archive Hotfix) +# Kontrollierter Cleanup eines NACHWEISLICH unvollstaendigen Recovery-Archive- +# Ordners. KEINE freie Pfadangabe. Nur der exakte Partial-Ordner fuer +# CURRENT_RECOVERY_FULL_IMAGE_ID. Verified Archive wird strikt DENY. +# Safety-Model (Punkt 9): Pfad exakt unter festem Root, Ordnername exakt aus +# CURRENT_RECOVERY_FULL_IMAGE_ID abgeleitet, kein freier Pfad, kein Symlink, +# kein Mountpoint, kein verified Archive, session.meta fehlt ODER INVALID/PARTIAL, +# CURRENT_RECOVERY_FULL_IMAGE_ID stimmt weiterhin, keine andere Recovery-ID. +# ============================================================================= +cleanup_partial_recovery_archive() { + expect_zero "$@" + echo "=== CLEANUP-PARTIAL-RECOVERY-ARCHIVE ($RUN_MODE) ===" + # Aktive Recovery-Identitaet aus kontrolliertem State (Punkt 17). Kein d2db. + load_recovery_state + local sdir sid base + sdir="$(role_arch_dir recovery)" # = $ARCHIVE_ROOT/recovery + sid="$RECOVERY_ARCHIVE_ID" # exakt aus CURRENT_RECOVERY_FULL_IMAGE_ID abgeleitet + base="$sdir/$sid" + echo " TARGET=$base" + echo " CURRENT_RECOVERY_FULL_IMAGE_ID=$RECOVERY_GOLD_IMAGE_ID" + # --- Safety-Checks (alle FAIL CLOSED) --- + # 1. Root exakt unter festem Recovery Archive Root + [ "$sdir" = "$ARCHIVE_ROOT/recovery" ] || die "cleanup: Root nicht exakt $ARCHIVE_ROOT/recovery (FAIL CLOSED)" + # 2. Ordnername exakt 12 hex (aus FULL_IMAGE_ID abgeleitet, kein freier Pfad) + [[ "$sid" =~ ^[a-f0-9]{12}$ ]] || die "cleanup: Ordnername ungueltig '$sid' (FAIL CLOSED)" + # 3. Ziel existiert + [ -e "$base" ] || die "cleanup: Ziel existiert nicht $base (FAIL CLOSED)" + # 4. kein Symlink + [ ! -L "$base" ] || die "cleanup: Ziel ist Symlink (FAIL CLOSED)" + # 5. kein Mountpoint + if mountpoint -q "$base" 2>/dev/null; then die "cleanup: Ziel ist Mountpoint (FAIL CLOSED)"; fi + # 6. CURRENT_RECOVERY_FULL_IMAGE_ID stimmt weiterhin (Tag->ID Gate) + local tagid + tagid="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)" + [ "$tagid" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "cleanup: Tag->ID Gate FAIL ($tagid != $RECOVERY_GOLD_IMAGE_ID) (FAIL CLOSED)" + # 7. kein verified Archive (Punkt 10: verified Archive strikt DENY) + # verify_archive macht bei FAIL intern exit 2 -> in Subshell kapseln, + # damit ein INVALID/PARTIAL-Ergebnis das Skript nicht beendet. + local verify_rc=0 + ( verify_archive recovery ) >/dev/null 2>&1 || verify_rc=$? + if [ "$verify_rc" -eq 0 ]; then + die "cleanup: Archive ist VERIFIED (verify-recovery-archive PASS) โ€” Cleanup strikt DENY (FAIL CLOSED)" + fi + # 8. Klassifikation: PARTIAL wenn session.meta fehlt; sonst INVALID (verify FAIL) + local classification + if [ ! -f "$base/session.meta" ]; then + classification="PARTIAL" + else + classification="INVALID" + fi + echo " CLASSIFICATION=$classification" + # 9. Nur PARTIAL/INVALID darf bereinigt werden (verified bereits oben DENY) + [ "$classification" = "PARTIAL" ] || [ "$classification" = "INVALID" ] || die "cleanup: Klassifikation ungueltig (FAIL CLOSED)" + # 10. keine andere Recovery-ID betroffen โ€” nur exakt dieser sid + local other + for other in "$sdir"/*/; do + [ -d "$other" ] || continue + local oname="${other%/}"; oname="${oname##*/}" + [ "$oname" = "$sid" ] && continue + die "cleanup: andere Recovery-ID betroffen: $oname (FAIL CLOSED)" + done + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo " WOULD_REMOVE=$base" + echo "DRY_RUN_PASS: cleanup-partial-recovery-archive wuerde $base entfernen (keine Aktion)." + exit 0 + fi + mode_or_die + # MUTATION: kontrollierter Cleanup des Partial-Ordners + rm -rf -- "$base" || die "cleanup: Entfernen fehlgeschlagen (FAIL CLOSED)" + [ ! -e "$base" ] || die "cleanup: Ziel nach Entfernen noch vorhanden (FAIL CLOSED)" + echo "CLEANUP-PARTIAL-RECOVERY-ARCHIVE OK: $base entfernt" + echo " Retry bereit: archive-recovery-image wird denselben CURRENT_RECOVERY_FULL_IMAGE_ID verwenden." +} verify_ready_cmd() { # $1 = role (read-only) local role="$1"; expect_zero "${@:2}" verify_ready "$role" || die "$(case $role in recovery) echo 'RECOVERY READY';; remediation) echo 'CANDIDATE READY';; esac): FAIL (FAIL CLOSED)" @@ -2395,8 +2473,8 @@ if [ "$SUBCMD" = "dry-run" ]; then SUBCMD="${1:-}" shift || true case "$SUBCMD" in - build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|recover-gold-build-source) ;; - *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image zulaessig" ;; + build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|cleanup-partial-recovery-archive|recover-gold-build-source) ;; + *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|cleanup-partial-recovery-archive zulaessig" ;; esac fi @@ -2806,6 +2884,7 @@ case "$SUBCMD" in pin-remediation-image) pin_image remediation "$@" ;; verify-recovery-archive) verify_archive_cmd recovery "$@" ;; verify-remediation-archive) verify_archive_cmd remediation "$@" ;; + cleanup-partial-recovery-archive) cleanup_partial_recovery_archive "$@" ;; verify-recovery-pin) verify_pin_cmd recovery "$@" ;; verify-remediation-pin) verify_pin_cmd remediation "$@" ;; verify-recovery-ready) verify_ready_cmd recovery "$@" ;; @@ -2822,6 +2901,6 @@ case "$SUBCMD" in verify-recovered-gold-build-source) verify_recovered_gold_build_source "$@";; wrapper-sha) expect_zero "$@"; sha256sum /usr/local/bin/rq-historical ;; - *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|verify-recovery-archive|verify-remediation-archive|verify-recovery-pin|verify-remediation-pin|verify-recovery-ready|verify-remediation-ready|dry-restore-recovery-image-archive|restore-recovery-image-archive|verify-deployed-gold-source|dry-run recover-gold-build-source|recover-gold-build-source|verify-recovered-gold-build-source" + *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|verify-recovery-archive|verify-remediation-archive|cleanup-partial-recovery-archive|verify-recovery-pin|verify-remediation-pin|verify-recovery-ready|verify-remediation-ready|dry-restore-recovery-image-archive|restore-recovery-image-archive|verify-deployed-gold-source|dry-run recover-gold-build-source|recover-gold-build-source|verify-recovered-gold-build-source" ;; esac