Phase 13.5: Persistent Host-SoT Patch Promotion Channel - promote-runner-patch, rollback-runner-patch (SHA-bound, atomic, FAIL CLOSED)
This commit is contained in:
parent
6bcad03e18
commit
223fa33f50
1 changed files with 183 additions and 2 deletions
|
|
@ -156,6 +156,19 @@ RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt-
|
||||||
CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
|
CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
|
||||||
CANDIDATE_TAG="historical-v2-historical-service:candidate-phase13-5-noop"
|
CANDIDATE_TAG="historical-v2-historical-service:candidate-phase13-5-noop"
|
||||||
|
|
||||||
|
# ------------------------------------------------- PERSISTENT-HOST-SOT-PROMOTION
|
||||||
|
# Phase 13.5 · Persistent Host-SoT Patch Promotion Channel. KEIN echter Promote
|
||||||
|
# durch Rain. Nur run_backfill_year.py. Feste Identitaeten, KEIN User-Input.
|
||||||
|
# PATCH_SOURCE=/opt/data/run_backfill_year.py existiert NUR im RQ-Container
|
||||||
|
# (hermes-red-queen), nicht auf dem VPS-Host -> fester docker cp aus dem Container.
|
||||||
|
PATCH_SOURCE_CONTAINER="hermes-red-queen"
|
||||||
|
PATCH_SOURCE_PATH="/opt/data/run_backfill_year.py"
|
||||||
|
PATCH_TARGET="/opt/historical-v2/app/run_backfill_year.py"
|
||||||
|
# Read-only ermittelt (29.08.): SHA256 des von Red Queen geprueften Patch-Artefakts (8292 B).
|
||||||
|
EXPECTED_RUNNER_PATCH_SHA256="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524599f0752"
|
||||||
|
# Fester Backup-Root fuer die Promotion-Session (root-owned, nicht world-readable).
|
||||||
|
PROMOTE_BACKUP_ROOT="/opt/rq-historical-sot-backups/pre-runner-remediation"
|
||||||
|
|
||||||
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
|
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
|
||||||
RUN_MODE=MUTATING
|
RUN_MODE=MUTATING
|
||||||
|
|
||||||
|
|
@ -811,6 +824,172 @@ rollback_to_recovery_gold() {
|
||||||
echo "ROLLBACK-TO-RECOVERY-GOLD OK. Container laeuft mit Recovery-Gold-Image."
|
echo "ROLLBACK-TO-RECOVERY-GOLD OK. Container laeuft mit Recovery-Gold-Image."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ================= PERSISTENT-HOST-SOT-PROMOTION-CHANNEL =====================
|
||||||
|
# Phase 13.5 · Persistent Host-SoT Patch Promotion. KEIN echter Promote durch Rain.
|
||||||
|
# Nur run_backfill_year.py. Feste Identitaeten, KEIN User-Input.
|
||||||
|
|
||||||
|
# Patch aus dem RQ-Container in eine Temp-Datei holen (read-only, kein Target-Touch).
|
||||||
|
fetch_patch() {
|
||||||
|
# $1 = Ausgabe-Temp-Datei
|
||||||
|
docker cp "$PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH" "$1" >/dev/null 2>&1 || die "Patch aus $PATCH_SOURCE_CONTAINER nicht lesbar (FAIL CLOSED)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pre-Promotion Guards (Punkt 5). Read-only. FAIL CLOSED.
|
||||||
|
promote_pre_guards() {
|
||||||
|
echo " [guard A] Patch Source existiert als reguläre Datei (im RQ-Container):"
|
||||||
|
docker exec "$PATCH_SOURCE_CONTAINER" sh -c "[ -f '$PATCH_SOURCE_PATH' ]" >/dev/null 2>&1 || die "Guard A FAIL: Patch-Source fehlt (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [guard B] kein Symlink (Source):"
|
||||||
|
docker exec "$PATCH_SOURCE_CONTAINER" sh -c "[ -L '$PATCH_SOURCE_PATH' ]" >/dev/null 2>&1 && die "Guard B FAIL: Source ist Symlink (FAIL CLOSED)" || echo " OK"
|
||||||
|
echo " [guard C] Source SHA == EXPECTED_RUNNER_PATCH_SHA256:"
|
||||||
|
local ps; ps="$(docker exec "$PATCH_SOURCE_CONTAINER" sha256sum "$PATCH_SOURCE_PATH" 2>/dev/null | awk '{print $1}')"
|
||||||
|
[ "$ps" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || die "Guard C FAIL: Source SHA $ps != erwartet (FAIL CLOSED)"
|
||||||
|
echo " OK ($ps)"
|
||||||
|
echo " [guard D] Target existiert als reguläre Datei:"
|
||||||
|
[ -f "$PATCH_TARGET" ] || die "Guard D FAIL: Target fehlt (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [guard E] kein Symlink (Target):"
|
||||||
|
[ -L "$PATCH_TARGET" ] && die "Guard E FAIL: Target ist Symlink (FAIL CLOSED)" || echo " OK"
|
||||||
|
echo " [guard F] Host-SoT Baseline unverändert (Target-SHA == Gold-SHA 70d3abb1…):"
|
||||||
|
local ts; ts="$(sha256sum "$PATCH_TARGET" 2>/dev/null | awk '{print $1}')"
|
||||||
|
[ "$ts" = "70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" ] || die "Guard F FAIL: Target-SHA $ts != Gold-Baseline (FAIL CLOSED)"
|
||||||
|
echo " OK ($ts)"
|
||||||
|
echo " [guard G] Recovery-Gold Image inspectable:"
|
||||||
|
docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Guard G FAIL (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [guard H] Recovery-Tag korrekt gebunden:"
|
||||||
|
local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)"
|
||||||
|
[ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "Guard H FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [guard I] verify-recovery-gold-image PASS:"
|
||||||
|
( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "Guard I FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)"
|
||||||
|
echo " [guard J] historical-db healthy:"
|
||||||
|
ensure_db_run && echo " OK"
|
||||||
|
echo " [guard K] kein Runner / kein Backfill aktiv:"
|
||||||
|
docker ps --format '{{.Names}}' | grep -qiE 'runner|backfill' && die "Guard K FAIL: Runner/Backfill aktiv (FAIL CLOSED)" || echo " OK"
|
||||||
|
echo " [guard L] Production Gates geschlossen:"
|
||||||
|
echo " OK (isolierter POC-Stack, kein Produktiv-Trading-Zugriff)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Promotion-Session-Backup (Punkt 6). Root-owned, nicht world-readable.
|
||||||
|
promote_backup() {
|
||||||
|
# $1 = Session-Timestamp
|
||||||
|
local ts="$1" bdir bfile
|
||||||
|
bdir="$PROMOTE_BACKUP_ROOT/$ts"
|
||||||
|
mkdir -p "$bdir" || die "Backup-Verzeichnis nicht erstellbar (FAIL CLOSED)"
|
||||||
|
chmod 700 "$bdir"
|
||||||
|
bfile="$bdir/run_backfill_year.py"
|
||||||
|
cp -a "$PATCH_TARGET" "$bfile" || die "Backup fehlgeschlagen (FAIL CLOSED)"
|
||||||
|
chown root:root "$bfile"; chmod 600 "$bfile"
|
||||||
|
echo "$bfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Promotion-Session-Metadaten (Punkt 6/8). Root-owned.
|
||||||
|
promote_session_meta() {
|
||||||
|
# $1 = Session-Timestamp, $2 = Backup-Datei, $3 = TARGET_SHA_BEFORE
|
||||||
|
local ts="$1" bfile="$2" before="$3" mfile
|
||||||
|
mfile="$PROMOTE_BACKUP_ROOT/$ts/session.meta"
|
||||||
|
{
|
||||||
|
echo "SESSION_TS=$ts"
|
||||||
|
echo "PATCH_SOURCE=$PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH"
|
||||||
|
echo "PATCH_TARGET=$PATCH_TARGET"
|
||||||
|
echo "EXPECTED_RUNNER_PATCH_SHA256=$EXPECTED_RUNNER_PATCH_SHA256"
|
||||||
|
echo "TARGET_SHA_BEFORE=$before"
|
||||||
|
echo "BACKUP_SHA=$(sha256sum "$bfile" | awk '{print $1}')"
|
||||||
|
echo "TARGET_OWNER=$(stat -c '%U' "$PATCH_TARGET")"
|
||||||
|
echo "TARGET_GROUP=$(stat -c '%G' "$PATCH_TARGET")"
|
||||||
|
echo "TARGET_MODE=$(stat -c '%a' "$PATCH_TARGET")"
|
||||||
|
} > "$mfile"
|
||||||
|
chown root:root "$mfile"; chmod 600 "$mfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- promote-runner-patch (MUTATION; nur Red Queen fuehrt aus) ----
|
||||||
|
promote_runner_patch() {
|
||||||
|
expect_zero "$@"
|
||||||
|
echo "=== PROMOTE-RUNNER-PATCH ($RUN_MODE) ==="
|
||||||
|
echo " SOURCE : $PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH"
|
||||||
|
echo " TARGET : $PATCH_TARGET"
|
||||||
|
echo " EXPECTED PATCH SHA : $EXPECTED_RUNNER_PATCH_SHA256"
|
||||||
|
local ts; ts="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo " Pre-Guards:"
|
||||||
|
promote_pre_guards
|
||||||
|
local tbefore; tbefore="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')"
|
||||||
|
echo " CURRENT TARGET SHA : $tbefore"
|
||||||
|
echo " geplante Backup-Position : $PROMOTE_BACKUP_ROOT/$ts/run_backfill_year.py"
|
||||||
|
echo " geplante eine Datei-Mutation : $PATCH_TARGET"
|
||||||
|
if [ "$RUN_MODE" = "DRY_RUN" ]; then
|
||||||
|
echo "DRY_RUN_PASS: wuerde Patch promoten (Backup + atomarer rename). KEINE Aktion."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mode_or_die
|
||||||
|
# Backup + Session-Metadaten
|
||||||
|
local bfile; bfile="$(promote_backup "$ts")"
|
||||||
|
promote_session_meta "$ts" "$bfile" "$tbefore"
|
||||||
|
# Patch in Temp-Datei im Zielverzeichnis holen + SHA pruefen
|
||||||
|
local tmpf; tmpf="$PATCH_TARGET.tmp.$$"
|
||||||
|
fetch_patch "$tmpf" || { rm -f "$tmpf"; die "Promotion: Patch nicht lesbar (FAIL CLOSED)"; }
|
||||||
|
local ps; ps="$(sha256sum "$tmpf" | awk '{print $1}')"
|
||||||
|
[ "$ps" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || { rm -f "$tmpf"; die "Promotion: Temp-SHA $ps != erwartet (FAIL CLOSED)"; }
|
||||||
|
# Besitzer/Mode exakt wie bestehendes Target herstellen
|
||||||
|
chown "$(stat -c '%U' "$PATCH_TARGET"):$(stat -c '%G' "$PATCH_TARGET")" "$tmpf"
|
||||||
|
chmod "$(stat -c '%a' "$PATCH_TARGET")" "$tmpf"
|
||||||
|
# atomarer rename
|
||||||
|
mv -f "$tmpf" "$PATCH_TARGET" || { rm -f "$tmpf"; die "Promotion: atomarer rename fehlgeschlagen (FAIL CLOSED)"; }
|
||||||
|
echo " Promotion ausgeführt (Backup=$bfile)."
|
||||||
|
echo " Post-Promotion Verify:"
|
||||||
|
local tafter; tafter="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')"
|
||||||
|
echo " TARGET_SHA_AFTER : $tafter"
|
||||||
|
[ "$tafter" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || die "Post-Promotion FAIL: Target-SHA != erwartet (FAIL CLOSED)"
|
||||||
|
echo " py_compile gegen persistenten Host-SoT Runner:"
|
||||||
|
python3 -m py_compile "$PATCH_TARGET" 2>&1 && echo " OK" || die "Post-Promotion FAIL: py_compile (FAIL CLOSED)"
|
||||||
|
echo "PROMOTE-RUNNER-PATCH OK. Host-SoT Runner ist jetzt der gepruefte Patch."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- rollback-runner-patch (MUTATION; nur Red Queen fuehrt aus) ----
|
||||||
|
rollback_runner_patch() {
|
||||||
|
expect_zero "$@"
|
||||||
|
echo "=== ROLLBACK-RUNNER-PATCH ($RUN_MODE) ==="
|
||||||
|
echo " TARGET : $PATCH_TARGET"
|
||||||
|
echo " Rollback-Sicherheit (Punkt 11):"
|
||||||
|
# gueltige Promotion-Session vorhanden?
|
||||||
|
local latest; latest="$(ls -1 "$PROMOTE_BACKUP_ROOT" 2>/dev/null | sort | tail -1 || true)"
|
||||||
|
[ -n "$latest" ] || die "Rollback FAIL: keine gueltige Promotion-Session (FAIL CLOSED)"
|
||||||
|
local mfile bfile
|
||||||
|
mfile="$PROMOTE_BACKUP_ROOT/$latest/session.meta"
|
||||||
|
bfile="$PROMOTE_BACKUP_ROOT/$latest/run_backfill_year.py"
|
||||||
|
[ -f "$mfile" ] || die "Rollback FAIL: Session-Metadaten fehlen (FAIL CLOSED)"
|
||||||
|
[ -f "$bfile" ] || die "Rollback FAIL: Backup fehlt (FAIL CLOSED)"
|
||||||
|
[ -L "$bfile" ] && die "Rollback FAIL: Backup ist Symlink (FAIL CLOSED)"
|
||||||
|
local bsha; bsha="$(sha256sum "$bfile" | awk '{print $1}')"
|
||||||
|
local tbefore; tbefore="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')"
|
||||||
|
echo " Session : $latest"
|
||||||
|
echo " Backup SHA : $bsha"
|
||||||
|
echo " aktueller Target-Zustand : $tbefore"
|
||||||
|
echo " geplante eine Datei-Mutation : $PATCH_TARGET (zurueck auf Backup)"
|
||||||
|
if [ "$RUN_MODE" = "DRY_RUN" ]; then
|
||||||
|
echo "DRY_RUN_PASS: wuerde Backup $bfile auf $PATCH_TARGET zurueckspielen. KEINE Aktion."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mode_or_die
|
||||||
|
# aktuellen Target-Zustand dokumentieren (vor Rollback)
|
||||||
|
local cur; cur="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')"
|
||||||
|
echo " aktueller Target-SHA vor Rollback : $cur"
|
||||||
|
# Backup in Temp + SHA pruefen + atomarer rename
|
||||||
|
local tmpf; tmpf="$PATCH_TARGET.tmp.$$"
|
||||||
|
cp -a "$bfile" "$tmpf" || die "Rollback: Backup nicht lesbar (FAIL CLOSED)"
|
||||||
|
local ps; ps="$(sha256sum "$tmpf" | awk '{print $1}')"
|
||||||
|
[ "$ps" = "$bsha" ] || { rm -f "$tmpf"; die "Rollback: Temp-SHA != Backup-SHA (FAIL CLOSED)"; }
|
||||||
|
chown "$(stat -c '%U' "$PATCH_TARGET"):$(stat -c '%G' "$PATCH_TARGET")" "$tmpf"
|
||||||
|
chmod "$(stat -c '%a' "$PATCH_TARGET")" "$tmpf"
|
||||||
|
mv -f "$tmpf" "$PATCH_TARGET" || { rm -f "$tmpf"; die "Rollback: atomarer rename fehlgeschlagen (FAIL CLOSED)"; }
|
||||||
|
local tafter; tafter="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')"
|
||||||
|
echo " TARGET_SHA_AFTER : $tafter"
|
||||||
|
# TARGET_SHA_BEFORE aus Session-Metadaten
|
||||||
|
local expected; expected="$(grep '^TARGET_SHA_BEFORE=' "$mfile" | cut -d= -f2)"
|
||||||
|
[ "$tafter" = "$expected" ] || die "Rollback FAIL: Target-SHA != TARGET_SHA_BEFORE (FAIL CLOSED)"
|
||||||
|
echo "ROLLBACK-RUNNER-PATCH OK. Target zurueck auf TARGET_SHA_BEFORE=$expected."
|
||||||
|
}
|
||||||
|
|
||||||
# ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ----
|
# ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ----
|
||||||
build_historical() {
|
build_historical() {
|
||||||
expect_zero "$@"
|
expect_zero "$@"
|
||||||
|
|
@ -921,7 +1100,7 @@ if [ "$SUBCMD" = "dry-run" ]; then
|
||||||
SUBCMD="${1:-}"
|
SUBCMD="${1:-}"
|
||||||
shift || true
|
shift || true
|
||||||
case "$SUBCMD" in
|
case "$SUBCMD" in
|
||||||
build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold) ;;
|
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) ;;
|
||||||
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;;
|
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
@ -1294,6 +1473,8 @@ case "$SUBCMD" in
|
||||||
verify-candidate-image) verify_candidate_image "$@" ;;
|
verify-candidate-image) verify_candidate_image "$@" ;;
|
||||||
recreate-candidate-safe) recreate_candidate_safe "$@" ;;
|
recreate-candidate-safe) recreate_candidate_safe "$@" ;;
|
||||||
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
|
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
|
||||||
|
promote-runner-patch) promote_runner_patch "$@" ;;
|
||||||
|
rollback-runner-patch) rollback_runner_patch "$@" ;;
|
||||||
build-historical) build_historical "$@" ;;
|
build-historical) build_historical "$@" ;;
|
||||||
create-build-exclusion) expect_zero "$@"
|
create-build-exclusion) expect_zero "$@"
|
||||||
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
|
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
|
||||||
|
|
@ -1321,6 +1502,6 @@ case "$SUBCMD" in
|
||||||
recreate-historical) recreate_historical "$@" ;;
|
recreate-historical) recreate_historical "$@" ;;
|
||||||
verify-gold-reproduction) verify_gold_reproduction "$@" ;;
|
verify-gold-reproduction) verify_gold_reproduction "$@" ;;
|
||||||
|
|
||||||
*) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion"
|
*) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue