From 3de75a5229cdede8738cec486c25bf3df36e303f Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 23 Jul 2026 03:23:42 -0400 Subject: [PATCH] =?UTF-8?q?granja:=20el=20dead-man=20del=20worker=20se=20m?= =?UTF-8?q?ata=20SOLO=20(sin=20hub)=20=E2=80=94=203=20bugs=20+=20gioser=20?= =?UTF-8?q?doble-blindado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El worker DEBE matarse solo, sin depender de la laptop (es la razón de ser del dead-man: vive en el worker; el volumen hace que morir no pierda nada). El reaper del hub queda sólo como último recurso. El dead-man estaba TRIPLEMENTE roto: 1. TOKEN EN EL FICHERO EQUIVOCADO: hay 2 caminos de creación con el token en lugares distintos (farm-up → /etc/hammer-deadman.env; harkaq-vol → /root/ .hcloud-token). La service lee sólo el primero ⇒ un worker del otro camino quedaba sin token, disparaba pero salía 1 "sin HCLOUD_TOKEN". Fix: deadman.sh busca el token EN CADENA (volumen primero, que es lo más persistente). 2. REGEX DEL ID ROTO: la API devuelve JSON con espacio ("id": 126, no "id":126) y el dead-man usaba '"id":[0-9]+' ⇒ NUNCA resolvía su id, aun con token válido. Fix: '"id":[[:space:]]*[0-9]+'. (Doblemente roto: por eso NUNCA murió.) 3. FIRING ≠ CAN-DELETE: farm-up sólo verificaba que el timer dispara. Ahora corre `deadman.sh --puede-borrar` (token en cadena + ve su id en la API) y si NO puede matarse, DESTRUYE el worker ahí mismo. Un worker que no se autodestruye es inadmisible ⇒ jamás debe existir. gioser DOBLE-BLINDADO en TODOS los sitios de borrado (lista negra por nombre + label role=hammer-worker), igual que farm-down ya tenía: reaper, farm-up-destroy, deadman, y el helper de harkaq-vol. "Ahí está nuestra vida." Verificado vivo (104d). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/farm/cosecha-cron.sh | 5 +++++ scripts/farm/deadman.sh | 30 +++++++++++++++++++++++++++++- scripts/farm/farm-up.sh | 36 ++++++++++++++++++++++-------------- scripts/farm/harkaq-vol.sh | 6 +++++- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index f84f61f1..3bf8ff49 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -90,6 +90,11 @@ if [ -s "$FLEET" ] && command -v hcloud >/dev/null 2>&1; then mkdir -p "$ROOT/work/.reaper"; sobreviven="" while read -r name ip; do [ -n "${name:-}" ] || continue + # LISTA NEGRA (capa 1 de 2, igual que el dead-man): gioser JAMÁS se toca, pase lo que pase. + # Segunda capa = el chequeo de label abajo. gioser no tiene label Y no matchea worker de la + # granja ⇒ imposible que muera por acá. "Ahí está nuestra vida" (2026-07-23). + case "$name" in *gioser*) echo "==> reaper: $name en LISTA NEGRA ⇒ intocable"; sobreviven="${sobreviven}${name} ${ip} +"; continue;; esac strike_f="$ROOT/work/.reaper/$name" # ¿el server siquiera existe? Si ya no está en hcloud (borrado a mano o por un ciclo previo), # NO cuesta € y no debe quedar en .fleet para siempre. Se dropea (no entra a `sobreviven`). diff --git a/scripts/farm/deadman.sh b/scripts/farm/deadman.sh index c55f1952..2fccaf7b 100644 --- a/scripts/farm/deadman.sh +++ b/scripts/farm/deadman.sh @@ -54,6 +54,34 @@ LOG="/var/log/hammer-deadman.log" log() { echo "$(date -u +%FT%TZ) deadman: $*" >> "$LOG"; } +# TOKEN ROBUSTO A CADA CAMINO DE CREACIÓN (incidente 2026-07-23). Hay DOS caminos que crean workers y +# ponen el token en ficheros DISTINTOS: `farm-up` → /etc/hammer-deadman.env (EnvironmentFile de la +# service); `harkaq-vol.sh` → /root/.hcloud-token (cloud-init). La service del snapshot lee sólo el +# primero ⇒ un worker nacido por el OTRO camino quedaba SIN token en su env, disparaba pero salía 1 +# "sin HCLOUD_TOKEN" y NUNCA se borraba (3.5h idle quemando €). El dead-man NO puede depender del hub +# para matarse (ésa es su razón de ser: vive en el worker). Así que busca el token en CADENA — el +# volumen primero, porque es lo más persistente (sobrevive al server, la idea del volumen del usuario). +: "${HCLOUD_TOKEN:=}" +if [ -z "$HCLOUD_TOKEN" ]; then + for f in /mnt/cosecha/.hcloud-token /root/.hcloud-token /etc/hcloud-token; do + [ -r "$f" ] || continue + HCLOUD_TOKEN=$(tr -d ' \t\n\r' < "$f" 2>/dev/null || true) + [ -n "$HCLOUD_TOKEN" ] && { log "token recuperado de $f (env vacío)"; break; } + done +fi +export HCLOUD_TOKEN + +# ¿PUEDE este worker borrarse? (token que VE su propio id en la API). firing ≠ can-delete: `farm-up` +# usa esto para no dejar vivo un worker que no puede matarse, en vez de descubrirlo idle 3.5h tarde. +if [ "${1:-}" = "--puede-borrar" ]; then + if [ -z "$HCLOUD_TOKEN" ]; then echo "NO: sin token en ninguna ubicación conocida"; exit 1; fi + id=$(curl -sS -H "Authorization: Bearer $HCLOUD_TOKEN" \ + "https://api.hetzner.cloud/v1/servers?name=$(hostname)" 2>/dev/null \ + | grep -oE '"id":[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+') + if [ -n "$id" ]; then echo "SÍ: puedo borrar mi id=$id"; exit 0 + else echo "NO: el token no resuelve mi propio id (¿nombre '$(hostname)' no está en el proyecto?)"; exit 1; fi +fi + if [ "${1:-}" = "--verificar" ]; then echo "══ ¿el dead-man switch está REALMENTE armado?" echo "── timer:"; systemctl is-active hammer-deadman.timer 2>/dev/null || echo " INACTIVO ⚠" @@ -143,7 +171,7 @@ fi id=$(curl -sS -H "Authorization: Bearer $HCLOUD_TOKEN" \ "https://api.hetzner.cloud/v1/servers?name=$self" 2>/dev/null \ - | grep -oE '"id":[0-9]+' | head -1 | cut -d: -f2) + | grep -oE '"id":[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+') if [ -z "$id" ]; then log "ERROR: no pude resolver mi propio id de server (¿nombre '$self' no está en el proyecto?)" exit 1 diff --git a/scripts/farm/farm-up.sh b/scripts/farm/farm-up.sh index 85938f54..6a3f2bc0 100755 --- a/scripts/farm/farm-up.sh +++ b/scripts/farm/farm-up.sh @@ -139,21 +139,29 @@ for k in $(seq 1 "$N"); do systemctl daemon-reload && systemctl enable --now hammer-deadman.timer" >/dev/null 2>&1 # EVIDENCIA, no fe: que el timer esté ARMADO se comprueba, no se asume (el cron de aquella vez # estaba "validado" y nunca disparó porque el PID 1 no tenía crond). - if $SSH root@"$ip" 'systemctl is-active hammer-deadman.timer' 2>/dev/null | grep -q active; then - echo " ✓ dead-man armado: $($SSH root@"$ip" 'systemctl list-timers hammer-deadman.timer --no-pager 2>/dev/null | sed -n 2p' | awk '{print $1, $2, $3}')" - # FIRING ≠ CAN-DELETE (2026-07-23): un worker quedó idle 3.5h quemando € porque el token en - # /etc/hammer-deadman.env estaba vacío ⇒ el dead-man DISPARABA pero salía 1 "sin HCLOUD_TOKEN" - # y nunca se borraba. Que el timer esté activo NO prueba que pueda matarse. Se verifica que su - # token VE su propio id en la API — lo MISMO que hace al morir. - if $SSH root@"$ip" '. /etc/hammer-deadman.env 2>/dev/null; [ -n "${HCLOUD_TOKEN:-}" ] && curl -sS -H "Authorization: Bearer $HCLOUD_TOKEN" "https://api.hetzner.cloud/v1/servers?name=$(hostname)" 2>/dev/null | grep -q "\"id\""' 2>/dev/null; then - echo " ✓ dead-man PUEDE borrarse (token válido, se ve en la API)" - else - echo " ⛔ dead-man NO PUEDE borrarse: token vacío/inválido en /etc/hammer-deadman.env." - echo " Solo, este worker quedaría idle PARA SIEMPRE. Lo cubre el reaper del hub" - echo " (cosecha-cron), pero sólo si el latido corre. Arreglar la provisión del token." - fi + # FIRING ≠ CAN-DELETE (2026-07-23): un worker quedó idle 3.5h quemando € porque su token no + # estaba donde la service lo lee ⇒ el dead-man DISPARABA pero salía 1 "sin HCLOUD_TOKEN" y nunca + # se borraba. Que el timer esté activo NO prueba que pueda matarse. El worker DEBE poder matarse + # SOLO, sin el hub — así que si NO PUEDE, no lo dejamos vivo: lo DESTRUIMOS acá mismo. Un worker + # que no se puede autodestruir es exactamente lo que el usuario prohíbe. `deadman.sh --puede-borrar` + # busca el token en cadena (env, volumen, /root) y prueba que ve su id en la API. + timer_ok=$($SSH root@"$ip" 'systemctl is-active hammer-deadman.timer' 2>/dev/null | grep -q active && echo 1) + puede=$($SSH root@"$ip" '/usr/local/bin/deadman.sh --puede-borrar' 2>/dev/null) + if [ -n "$timer_ok" ] && printf '%s' "$puede" | grep -q '^SÍ'; then + echo " ✓ dead-man ARMADO y PUEDE borrarse solo ($puede) — no depende del hub" else - echo " ⚠ el timer NO quedó activo ⇒ este worker puede quedarse idle para siempre. Revisar." + echo " ⛔ $name NO puede autodestruirse (timer=${timer_ok:-no} · $puede)." + echo " Un worker que no se mata solo es INADMISIBLE ⇒ lo DESTRUYO ahora para no dejarlo idle." + # LISTA NEGRA (capa 1 de 2): gioser JAMÁS se toca. farm-up sólo crea hworker-N, pero defensa + # en profundidad — "ahí está nuestra vida". Segunda capa = el label check de abajo. + case "$name" in *gioser*) echo " ⛔ $name en LISTA NEGRA ⇒ NO lo toco. Revisá a mano."; continue;; esac + if hcloud server describe "$name" -o format='{{.Labels}}' 2>/dev/null | grep -q hammer-worker; then + hcloud server delete "$name" >/dev/null 2>&1 && echo " ☠ $name destruido (label verificado)." + else + echo " ⛔ $name sin label hammer-worker ⇒ NO lo toco (protegido); borralo a mano." + fi + grep -v "^$name " "$FLEET" > "$FLEET.tmp" 2>/dev/null && mv "$FLEET.tmp" "$FLEET" + continue fi fi echo " ✓ $name muele la cola actual" diff --git a/scripts/farm/harkaq-vol.sh b/scripts/farm/harkaq-vol.sh index a7a28f09..665e1b57 100755 --- a/scripts/farm/harkaq-vol.sh +++ b/scripts/farm/harkaq-vol.sh @@ -122,7 +122,11 @@ collect) | sed 's/^/ verdicts en el volumen: /' mkdir -p work/harkaq-campana rsync -az -e "$SSH" "root@$ip:/mnt/cosecha/verdicts/" work/harkaq-campana/ 2>/dev/null || true - [ -n "$helper" ] && { echo "== destruyo el helper"; hcloud server delete "$helper" >/dev/null; } + # El helper lo nombra este script, nunca es gioser — pero guarda igual ("ahí está nuestra vida"). + case "$helper" in + *gioser*) echo "== ⛔ helper matchea gioser?! ABORTO el delete" ;; + ?*) echo "== destruyo el helper"; hcloud server delete "$helper" >/dev/null ;; + esac # NO se corre el harvest acá: harvest-harkaq.sh rsync-ea de un WORKER, y en este punto los # verdicts ya están locales (los trajo el rsync del volumen). Clasificarlos es un paso aparte # —lo hace el hub con el store COMPLETO (el worker mide, el hub clasifica)— y su edición de