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_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=MUTATING
|
||||
|
||||
|
|
@ -811,6 +824,172 @@ rollback_to_recovery_gold() {
|
|||
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() {
|
||||
expect_zero "$@"
|
||||
|
|
@ -921,7 +1100,7 @@ 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) ;;
|
||||
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" ;;
|
||||
esac
|
||||
fi
|
||||
|
|
@ -1294,6 +1473,8 @@ case "$SUBCMD" in
|
|||
verify-candidate-image) verify_candidate_image "$@" ;;
|
||||
recreate-candidate-safe) recreate_candidate_safe "$@" ;;
|
||||
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
|
||||
promote-runner-patch) promote_runner_patch "$@" ;;
|
||||
rollback-runner-patch) rollback_runner_patch "$@" ;;
|
||||
build-historical) build_historical "$@" ;;
|
||||
create-build-exclusion) expect_zero "$@"
|
||||
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
|
||||
|
|
@ -1321,6 +1502,6 @@ case "$SUBCMD" in
|
|||
recreate-historical) recreate_historical "$@" ;;
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue