granja: el dead-man del worker se mata SOLO (sin hub) — 3 bugs + gioser doble-blindado
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`).
|
||||
|
||||
+29
-1
@@ -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
|
||||
|
||||
+22
-14
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user