#!/usr/bin/env bash # AWOOOI P3 controlled release gate. # Read-only: this script never starts, stops, restarts, deletes, or modifies services. set -uo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT_DIR" SSH_BATCH_MODE=${SSH_BATCH_MODE:-yes} SSH_OPTS=(-o BatchMode="$SSH_BATCH_MODE" -o ConnectTimeout=6) NO_COLOR=0 SKIP_COLD_START_GATE=0 LOAD5_PER_CORE_LIMIT="${LOAD5_PER_CORE_LIMIT:-1.0}" LOAD15_PER_CORE_LIMIT="${LOAD15_PER_CORE_LIMIT:-1.0}" JOB_CONTAINER_CPU_LIMIT="${JOB_CONTAINER_CPU_LIMIT:-1.0}" TEXTFILE_MAX_AGE_SECONDS="${TEXTFILE_MAX_AGE_SECONDS:-300}" usage() { cat <<'USAGE' Usage: bash scripts/reboot-recovery/p3-controlled-release-gate.sh [options] Options: --skip-cold-start-gate Do not run the full P0/P1/P2 read-only gate first. --no-color Disable ANSI colors. -h, --help Show this help. Environment overrides: LOAD5_PER_CORE_LIMIT=1.0 LOAD15_PER_CORE_LIMIT=1.0 JOB_CONTAINER_CPU_LIMIT=1.0 TEXTFILE_MAX_AGE_SECONDS=300 USAGE } while [ "$#" -gt 0 ]; do case "$1" in --skip-cold-start-gate) SKIP_COLD_START_GATE=1 ;; --no-color) NO_COLOR=1 ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 64 ;; esac shift done if [ "$NO_COLOR" = "1" ]; then RED="" GREEN="" YELLOW="" BLUE="" NC="" else RED=$'\033[0;31m' GREEN=$'\033[0;32m' YELLOW=$'\033[1;33m' BLUE=$'\033[0;34m' NC=$'\033[0m' fi PASS=0 WARN=0 FAIL=0 section() { printf "\n%s=== %s ===%s\n" "$BLUE" "$1" "$NC" } ok() { PASS=$((PASS + 1)) printf "%sOK%s %s\n" "$GREEN" "$NC" "$*" } warn() { WARN=$((WARN + 1)) printf "%sWARN%s %s\n" "$YELLOW" "$NC" "$*" } blocked() { FAIL=$((FAIL + 1)) printf "%sBLOCKED%s %s\n" "$RED" "$NC" "$*" } ssh_cmd() { local target="$1" local cmd="$2" ssh "${SSH_OPTS[@]}" "$target" "$cmd" } float_le() { awk -v a="$1" -v b="$2" 'BEGIN { exit !(a <= b) }' } check_cold_start_gate() { section "P0/P1/P2 cold-start gate" if [ "$SKIP_COLD_START_GATE" -eq 1 ]; then warn "cold-start gate skipped by operator option" return fi SSH_BATCH_MODE=yes bash scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1 >/tmp/awoooi-p3-cold-start-gate.log 2>&1 local rc=$? local summary blocked_count warn_count summary=$(grep -E '^PASS=[0-9]+ WARN=[0-9]+ BLOCKED=[0-9]+' /tmp/awoooi-p3-cold-start-gate.log | tail -1 || true) blocked_count=$(awk -F'BLOCKED=' '/^PASS=/ {print $2}' <<<"$summary") warn_count=$(awk -F'WARN=' '/^PASS=/ {split($2,a," "); print a[1]}' <<<"$summary") if [ "$rc" -eq 0 ]; then ok "cold-start gate is GREEN" elif [ "${blocked_count:-1}" = "0" ]; then warn "cold-start gate is DEGRADED but not blocked: ${summary:-summary unavailable}" else blocked "cold-start gate has blocked items: ${summary:-summary unavailable}; see /tmp/awoooi-p3-cold-start-gate.log" fi } check_host_load() { local label="$1" local target="$2" local out load5 load15 cores load5_per_core load15_per_core section "$label load gate" if ! out=$(ssh_cmd "$target" 'read _ load5 load15 _ < /proc/loadavg; cores=$(nproc); awk -v l5="$load5" -v l15="$load15" -v c="$cores" "BEGIN {printf \"LOAD5 %.4f LOAD15 %.4f CORES %d LOAD5_PER_CORE %.6f LOAD15_PER_CORE %.6f\\n\", l5, l15, c, l5/c, l15/c}"' 2>&1); then blocked "$label load check unavailable" echo "$out" return fi echo "$out" load5_per_core=$(awk '/LOAD5_PER_CORE/ {for (i=1;i<=NF;i++) if ($i=="LOAD5_PER_CORE") print $(i+1)}' <<<"$out") load15_per_core=$(awk '/LOAD15_PER_CORE/ {for (i=1;i<=NF;i++) if ($i=="LOAD15_PER_CORE") print $(i+1)}' <<<"$out") if float_le "$load5_per_core" "$LOAD5_PER_CORE_LIMIT"; then ok "$label load5/core <= $LOAD5_PER_CORE_LIMIT" else blocked "$label load5/core too high for P3 release" fi if float_le "$load15_per_core" "$LOAD15_PER_CORE_LIMIT"; then ok "$label load15/core <= $LOAD15_PER_CORE_LIMIT" else blocked "$label load15/core too high for P3 release" fi } check_textfiles() { section "textfile freshness" local out if out=$(ssh_cmd "wooo@192.168.0.110" ' now=$(date +%s) for f in /home/wooo/node_exporter_textfiles/docker_stats.prom /home/wooo/node_exporter_textfiles/systemd_units.prom /home/wooo/node_exporter_textfiles/storage_health.prom /home/wooo/node_exporter_textfiles/backup_health.prom /home/wooo/node_exporter_textfiles/cold_start_recovery.prom; do if [ -f "$f" ]; then echo "$(basename "$f") $((now - $(stat -c %Y "$f")))" else echo "$(basename "$f") missing" fi done ' 2>&1); then while read -r file age; do [ -n "${file:-}" ] || continue max_age="$TEXTFILE_MAX_AGE_SECONDS" [ "$file" = "cold_start_recovery.prom" ] && max_age=900 [ "$file" = "backup_health.prom" ] && max_age=900 if [ "$age" = "missing" ]; then blocked "110 $file missing" elif [ "$age" -le "$max_age" ]; then ok "110 $file fresh age=${age}s" else blocked "110 $file stale age=${age}s" fi done <<<"$out" else blocked "110 textfile freshness check unavailable" echo "$out" fi if out=$(ssh_cmd "ollama@192.168.0.188" ' now=$(date +%s) for f in /home/ollama/node_exporter_textfiles/docker_stats.prom /home/ollama/node_exporter_textfiles/docker_restart_count.prom /home/ollama/node_exporter_textfiles/storage_health.prom /home/ollama/node_exporter_textfiles/backup.prom /home/ollama/node_exporter_textfiles/backup_health.prom; do if [ -f "$f" ]; then echo "$(basename "$f") $((now - $(stat -c %Y "$f")))" else echo "$(basename "$f") missing" fi done ' 2>&1); then while read -r file age; do [ -n "${file:-}" ] || continue max_age="$TEXTFILE_MAX_AGE_SECONDS" [ "$file" = "backup.prom" ] && max_age=90000 [ "$file" = "backup_health.prom" ] && max_age=900 if [ "$age" = "missing" ]; then blocked "188 $file missing" elif [ "$age" -le "$max_age" ]; then ok "188 $file fresh age=${age}s" else blocked "188 $file stale age=${age}s" fi done <<<"$out" else blocked "188 textfile freshness check unavailable" echo "$out" fi } check_backup_health() { section "backup health gate" local label target file out stale missing_cron missing_script failed_count integrity_stale for spec in \ "110|wooo@192.168.0.110|/home/wooo/node_exporter_textfiles/backup_health.prom" \ "188|ollama@192.168.0.188|/home/ollama/node_exporter_textfiles/backup_health.prom"; do label=${spec%%|*} target=${spec#*|} target=${target%%|*} file=${spec##*|} if ! out=$(ssh_cmd "$target" " if [ ! -f '$file' ]; then echo 'BACKUP_HEALTH missing' exit 0 fi awk ' /^awoooi_backup_job_fresh/ {total += 1; stale += (\$2 == 0)} /^awoooi_backup_job_configured/ {missing_cron += (\$2 == 0)} /^awoooi_backup_script_present/ {missing_script += (\$2 == 0)} /^awoooi_backup_last_run_failed_count/ {failed += \$2} /^awoooi_backup_integrity_fresh/ {integrity_total += 1; integrity_stale += (\$2 == 0)} END {printf \"BACKUP_HEALTH total=%d stale=%d missing_cron=%d missing_script=%d failed_count=%d integrity_total=%d integrity_stale=%d\\n\", total, stale, missing_cron, missing_script, failed, integrity_total, integrity_stale} ' '$file' " 2>&1); then blocked "$label backup health check unavailable" echo "$out" continue fi echo "$label $out" if grep -q "BACKUP_HEALTH missing" <<<"$out"; then blocked "$label backup_health.prom missing" continue fi stale=$(awk -F'stale=' '{split($2,a," "); print a[1]+0}' <<<"$out") missing_cron=$(awk -F'missing_cron=' '{split($2,a," "); print a[1]+0}' <<<"$out") missing_script=$(awk -F'missing_script=' '{split($2,a," "); print a[1]+0}' <<<"$out") failed_count=$(awk -F'failed_count=' '{split($2,a," "); print a[1]+0}' <<<"$out") integrity_stale=$(awk -F'integrity_stale=' '{split($2,a," "); print a[1]+0}' <<<"$out") [ "$stale" -eq 0 ] && ok "$label expected backups are fresh" || blocked "$label expected backup jobs are stale" [ "$missing_cron" -eq 0 ] && ok "$label expected backup crons are configured" || blocked "$label expected backup cron config missing" [ "$missing_script" -eq 0 ] && ok "$label expected backup scripts are present" || blocked "$label expected backup scripts missing" if [ "$label" = "110" ]; then [ "$integrity_stale" -eq 0 ] && ok "110 backup integrity and restore drill are fresh" || blocked "110 backup integrity or restore drill stale" [ "$failed_count" -eq 0 ] && ok "110 latest aggregate backup had no failed components" || warn "110 latest aggregate backup still records failed components; rerun backup-all after fixes" fi done } check_storage_health() { section "storage health gate" local label target file out root_readonly current_errors previous_errors fsck_errors for spec in \ "110|wooo@192.168.0.110|/home/wooo/node_exporter_textfiles/storage_health.prom" \ "188|ollama@192.168.0.188|/home/ollama/node_exporter_textfiles/storage_health.prom"; do label=${spec%%|*} target=${spec#*|} target=${target%%|*} file=${spec##*|} if ! out=$(ssh_cmd "$target" " if [ ! -f '$file' ]; then echo 'STORAGE_HEALTH missing' exit 0 fi awk ' /^awoooi_host_root_filesystem_readonly/ {root += \$2} /^awoooi_host_storage_error_count/ && /boot=\"current\"/ {current += \$2} /^awoooi_host_storage_error_count/ && /boot=\"previous\"/ {previous += \$2} /^awoooi_host_storage_error_count/ && /boot=\"last-fsck-log\"/ {fsck += \$2} END {printf \"STORAGE_HEALTH root_readonly=%d current=%d previous=%d fsck=%d\\n\", root, current, previous, fsck} ' '$file' " 2>&1); then blocked "$label storage health check unavailable" echo "$out" continue fi echo "$label $out" if grep -q "STORAGE_HEALTH missing" <<<"$out"; then blocked "$label storage_health.prom missing" continue fi root_readonly=$(awk -F'root_readonly=' '{split($2,a," "); print a[1]+0}' <<<"$out") current_errors=$(awk -F'current=' '{split($2,a," "); print a[1]+0}' <<<"$out") previous_errors=$(awk -F'previous=' '{split($2,a," "); print a[1]+0}' <<<"$out") fsck_errors=$(awk -F'fsck=' '{split($2,a," "); print a[1]+0}' <<<"$out") [ "$root_readonly" -eq 0 ] && ok "$label root filesystem is writable" || blocked "$label root filesystem is read-only" [ "$current_errors" -eq 0 ] && ok "$label current boot has no storage error evidence" || blocked "$label current boot has storage error evidence" [ "$previous_errors" -eq 0 ] && ok "$label previous boot has no storage error evidence" || warn "$label previous boot has storage error evidence; keep fsck/backup follow-up open" [ "$fsck_errors" -eq 0 ] && ok "$label fsck logs have no retained error evidence" || warn "$label fsck logs retain error evidence; verify offline fsck/backup status" done } check_runner_guardrails() { section "runner/CD guardrails" local out bad if ! out=$(ssh_cmd "wooo@192.168.0.110" ' bad=0 for u in $(systemctl list-units "actions.runner.*" --all --no-legend --plain 2>/dev/null | awk "{print \$1}"); do watchdog=$(systemctl show "$u" -p WatchdogUSec --value) quota=$(systemctl show "$u" -p CPUQuotaPerSecUSec --value) memory=$(systemctl show "$u" -p MemoryMax --value) state=$(systemctl show "$u" -p ActiveState --value) echo "$u watchdog=$watchdog quota=$quota memory=$memory state=$state" [ "$watchdog" = "0" ] || bad=1 [ "$quota" != "infinity" ] && [ "$quota" != "0" ] || bad=1 [ "$memory" != "infinity" ] && [ "$memory" != "0" ] || bad=1 done echo "BAD_RUNNER_GUARDRAILS $bad" ' 2>&1); then blocked "runner guardrail check unavailable" echo "$out" return fi echo "$out" grep -q "BAD_RUNNER_GUARDRAILS 0" <<<"$out" && ok "all discovered runner units have watchdog disabled and CPU/memory limits" || blocked "runner guardrails incomplete" } check_job_containers() { section "active job container CPU" local out if ! out=$(ssh_cmd "wooo@192.168.0.110" ' names=$(docker ps --format "{{.Names}}" | grep -E "^(GITEA-ACTIONS-|awoooi-cd-)" || true) if [ -z "$names" ]; then echo "NO_ACTIVE_JOB_CONTAINERS" exit 0 fi for name in $names; do cpu=$(docker stats "$name" --no-stream --format "{{.CPUPerc}}" | tr -d "%" | awk "{printf \"%.6f\", \$1 / 100}") echo "JOB_CONTAINER $name cpu_cores=$cpu" done ' 2>&1); then blocked "job container CPU check unavailable" echo "$out" return fi echo "$out" if grep -q "NO_ACTIVE_JOB_CONTAINERS" <<<"$out"; then ok "no active Gitea/CD job containers" return fi local bad_count bad_count=$(awk -v limit="$JOB_CONTAINER_CPU_LIMIT" -F'cpu_cores=' '/^JOB_CONTAINER / {if (($2 + 0) > limit) bad++} END {print bad+0}' <<<"$out") if [ "$bad_count" -eq 0 ]; then ok "active job containers are below ${JOB_CONTAINER_CPU_LIMIT} CPU cores" else blocked "$bad_count active job container(s) exceed ${JOB_CONTAINER_CPU_LIMIT} CPU cores" fi } check_high_load_services() { section "high-load service health" local out if out=$(ssh_cmd "ollama@192.168.0.188" ' echo "ollama-systemd $(systemctl is-active ollama 2>/dev/null || true)" echo "ollama-api $(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://127.0.0.1:11434/api/tags || true)" docker inspect -f "momo-scheduler {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" momo-scheduler 2>/dev/null || true docker inspect -f "litellm {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" litellm 2>/dev/null || true docker inspect -f "signoz-clickhouse {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" signoz-clickhouse 2>/dev/null || true ' 2>&1); then echo "$out" grep -q "ollama-systemd active" <<<"$out" && ok "188 Ollama systemd active" || blocked "188 Ollama systemd inactive" grep -q "ollama-api 200" <<<"$out" && ok "188 Ollama API reachable" || blocked "188 Ollama API not reachable" grep -q "momo-scheduler running healthy" <<<"$out" && ok "188 momo-scheduler healthy" || blocked "188 momo-scheduler not healthy" grep -Eq "litellm running( |$)" <<<"$out" && ok "188 litellm running" || blocked "188 litellm not running" grep -q "signoz-clickhouse running healthy" <<<"$out" && ok "188 SignOz ClickHouse healthy" || warn "188 SignOz ClickHouse health not confirmed" else blocked "188 high-load service check unavailable" echo "$out" fi if out=$(ssh_cmd "wooo@192.168.0.110" ' docker inspect -f "sentry-clickhouse {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" sentry-self-hosted-clickhouse-1 2>/dev/null || true docker inspect -f "sentry-kafka {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" sentry-self-hosted-kafka-1 2>/dev/null || true docker ps --format "{{.Names}} {{.Status}}" | grep -E "sentry-self-hosted-(snuba|events|transactions|generic|metrics|subscription).*consumer" | head -20 || true ' 2>&1); then echo "$out" grep -q "sentry-clickhouse running healthy" <<<"$out" && ok "110 Sentry ClickHouse healthy" || blocked "110 Sentry ClickHouse not healthy" grep -q "sentry-kafka running healthy" <<<"$out" && ok "110 Sentry Kafka healthy" || blocked "110 Sentry Kafka not healthy" grep -q "Restarting" <<<"$out" && blocked "110 Sentry consumers include restarting containers" || ok "110 sampled Sentry consumers are not restarting" else blocked "110 high-load service check unavailable" echo "$out" fi } summary() { section "summary" echo "PASS=$PASS WARN=$WARN BLOCKED=$FAIL" if [ "$FAIL" -gt 0 ]; then echo "Result: HOLD_P3_RELEASE. Do not release runner/CD/crawlers/consumers further." return 1 fi if [ "$WARN" -gt 0 ]; then echo "Result: P3_RELEASE_WITH_CAUTION. Proceed only with operator review." return 0 fi echo "Result: P3_RELEASE_READY. Controlled high-load work release is allowed." } echo "AWOOOI P3 controlled release gate" date '+%Y-%m-%d %H:%M:%S %Z' echo "Limits: load5/core<=$LOAD5_PER_CORE_LIMIT load15/core<=$LOAD15_PER_CORE_LIMIT job_container_cpu<=$JOB_CONTAINER_CPU_LIMIT" check_cold_start_gate check_host_load "110" "wooo@192.168.0.110" check_host_load "188" "ollama@192.168.0.188" check_textfiles check_storage_health check_backup_health check_runner_guardrails check_job_containers check_high_load_services summary