La cabecera decía 'def 408909310' y la línea 38 usa 417847948 desde el 2026-08-08. La 408909310 es justo la que NO traía rust ni go — la que dejaba fuera al 76% del catálogo — así que el comentario apuntaba a la imagen rota. Un comentario que nombra un id concreto es una referencia, no prosa: si alguien lo lee y re-apunta IMAGE a mano, revive el bug que el propio fichero documenta debajo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
280 lines
19 KiB
Bash
Executable File
280 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# farm-up.sh [N] — levanta N workers EFÍMEROS desde el snapshot golden y los alinea con la cola
|
|
# actual del laptop. Modelo hub-and-spoke: el worker NO tiene secretos (ni firma ni gitea); es
|
|
# compute puro y descartable. La imagen 'hammer-golden' trae el toolchain + el store sellado
|
|
# horneados ⇒ el worker arranca con TODO el catálogo cacheado (cache-hit instantáneo), sólo
|
|
# construye lo NUEVO que el hub le empuje.
|
|
#
|
|
# Flujo: scripts/farm/farm-up.sh 3 # crea hworker-1..3, los alimenta con la cola actual
|
|
# ... (muelen) ...
|
|
# scripts/farm/farm-down.sh # cosecha su store al laptop y los DESTRUYE
|
|
#
|
|
# La flota viva se registra en scripts/farm/.fleet ("<name> <ip>" por línea; gitignored).
|
|
# Idempotente al escalar: re-correr con más N agrega workers sin pisar los vivos.
|
|
#
|
|
# Env: IMAGE (snapshot, def 417847948=hammer-golden-rust-go-2026-08-08), TYPE (def ccx23),
|
|
# LOCATION
|
|
# (def hel1), SSHKEY (llave del proyecto Hetzner, def desarrollo@jlsoltech.com), SSH_KEY
|
|
# (privada local p/ SSH, def ~/.ssh/github5), REMOTE (def /opt/hammer).
|
|
set -euo pipefail
|
|
|
|
N="${1:-1}"
|
|
# ── GOLDEN 417847948 (2026-08-08): la anterior NO TENÍA RUST NI GO ─────────────────────────────
|
|
# `hammer` invoca `cargo vendor` (y el toolchain Go) en el HOST, no dentro del sandbox: el vendoreo
|
|
# pasa ANTES de entrar a la caja. La golden vieja (408909310) no los traía, así que la granja no
|
|
# podía construir NINGUNA receta Rust ni Go — o sea COSMIC entero, las 228 Rust y las 362 Go del
|
|
# corpus: **el 76% del catálogo sólo se podía construir en el hub**, una sola máquina.
|
|
#
|
|
# Esta imagen añade cargo/rustc 1.96.0 y go 1.26.4, **las mismas versiones que el hub**, para no
|
|
# introducir un skew de toolchain nuevo mientras se arregla otro. Verificado antes de snapshotear:
|
|
# `cosmic-bg` —que minutos antes moría con `spawn cargo vendor: No such file`— sella en el worker.
|
|
#
|
|
# NO relaja la hermeticidad: cargo/go son herramientas de FETCH, del mismo orden que `git` para
|
|
# clonar. El build sigue ocurriendo dentro del sandbox con el árbol ya vendoreado. Es lo contrario
|
|
# del caso «no engordar el rootfs del worker», que habla del rootfs DEL SANDBOX.
|
|
#
|
|
# Trae además el store del worker al momento del snapshot (KDE y GNOME reconstruidos el 2026-08-08),
|
|
# así que los workers nuevos arrancan con más caché que antes.
|
|
IMAGE="${IMAGE:-417847948}"
|
|
TYPE="${TYPE:-ccx23}"
|
|
LOCATION="${LOCATION:-hel1}"
|
|
SSHKEY="${SSHKEY:-desarrollo@jlsoltech.com}"
|
|
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
|
|
REMOTE="${REMOTE:-/opt/hammer}"
|
|
# MISMO volumen que harkaq-vol.sh, a propósito: eran DOS caminos de crear workers y sólo uno tenía
|
|
# volumen+automuerte. hworker-4 nació por ÉSTE (farm-up) y por eso quedó 5 días idle con 37G que
|
|
# nadie salvó — la protección existía y este camino la esquivaba. Compartir el volumen hace que
|
|
# `harkaq-vol.sh collect` y `close` cosechen y clausuren lo de AMBOS caminos.
|
|
VOL_NAME="${VOL_NAME:-harkaq-cosecha}"
|
|
VOL_SIZE="${VOL_SIZE:-100}" # GB. ~€0.044/GB/mes ⇒ 100G ≈ €4.4/mes MIENTRAS exista.
|
|
# El baseline €0 lo da `harkaq-vol.sh close`, que es
|
|
# tuyo: el volumen es el único que sobrevive a propósito.
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
FLEET="$ROOT/scripts/farm/.fleet"
|
|
# Workers efímeros RECICLAN IPs de Hetzner ⇒ un known_hosts persistente choca ("HOST KEY CHANGED")
|
|
# y accept-new RECHAZA la clave cambiada, colgando el `until ssh true` para siempre. Como el modelo
|
|
# es hub-and-spoke (el worker NO tiene secretos: compute puro descartable), saltamos la verificación
|
|
# de host y no ensuciamos known_hosts (/dev/null). No hay superficie MITM que proteger acá.
|
|
SSH="ssh -i $SSH_KEY -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR -o ConnectTimeout=10"
|
|
cd "$ROOT"
|
|
touch "$FLEET"
|
|
|
|
# ── GUARDIÁN DEL VOLUMEN (2026-08-23) ─────────────────────────────────────────────────────────
|
|
# El chequeo de más abajo YA detectaba este fallo — y sólo imprimía un ⚠ mientras el script seguía
|
|
# adelante: armaba el dead-man y sembraba la cola igual. La noche del 2026-08-22 eso costó 21 G y
|
|
# 397 artefactos sellados. hworker-2 nació sin volumen (lo tenía hworker-1: un volumen de Hetzner se
|
|
# adjunta a UN server), construyó el shard 1/2 entero en su disco raíz y el dead-man se lo llevó a
|
|
# las 03:27Z. El aviso salió impreso, con estas mismas palabras, y no lo leyó nadie: el hub estaba
|
|
# esperando una respuesta humana que llegó después del borrado.
|
|
#
|
|
# La lección no es "avisar mejor": es que un aviso que no detiene el pipeline no es un guardián
|
|
# cuando nadie mira. Sembrar trabajo en un disco efímero es peor que no crear el worker, porque
|
|
# gasta horas de CPU y las tira. Ahora CORTA.
|
|
sin_volumen() {
|
|
local name="$1" motivo="$2"
|
|
echo " ✗ $name: $motivo" >&2
|
|
echo " Sin volumen, lo que selle vive en disco EFÍMERO y el dead-man se lo lleva entero." >&2
|
|
echo " NO lo siembro. Salidas:" >&2
|
|
echo " · un volumen POR worker: VOL_NAME=harkaq-cosecha-$name scripts/farm/farm-up.sh 1" >&2
|
|
echo " · cosechar y clausurar el actual: scripts/farm/harkaq-vol.sh collect (luego close)" >&2
|
|
echo " · worker deliberadamente desechable: SIN_VOLUMEN_OK=1 scripts/farm/farm-up.sh …" >&2
|
|
if [ "${SIN_VOLUMEN_OK:-0}" = "1" ]; then
|
|
echo " SIN_VOLUMEN_OK=1 ⇒ sigo igual: este worker construye a disco efímero." >&2
|
|
return 0
|
|
fi
|
|
# El server acaba de nacer y todavía no tiene nada dentro. Dejarlo vivo sería un idle facturando
|
|
# (es el caso hworker-4, 5 días), así que se borra — con el MISMO blindaje que el dead-man y
|
|
# farm-down: sólo si lleva el label role=hammer-worker. gioser no lo lleva ⇒ jamás cae por acá.
|
|
if hcloud server describe "$name" -o format='{{.Labels}}' 2>/dev/null | grep -q hammer-worker; then
|
|
if hcloud server delete "$name" >/dev/null 2>&1; then
|
|
echo " $name borrado (recién creado, vacío): sin factura colgando." >&2
|
|
sed -i "/^$name /d" "$FLEET" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
# índice de arranque: el mayor hworker-N vivo + 1 (para escalar sin colisión de nombres)
|
|
base=$(sed -n 's/^hworker-\([0-9]*\) .*/\1/p' "$FLEET" | sort -n | tail -1)
|
|
base="${base:-0}"
|
|
|
|
for k in $(seq 1 "$N"); do
|
|
i=$((base + k)); name="hworker-$i"
|
|
echo "==> creando $name ($TYPE @ $LOCATION desde image $IMAGE)"
|
|
hcloud server create --name "$name" --image "$IMAGE" --type "$TYPE" \
|
|
--location "$LOCATION" --ssh-key "$SSHKEY" --label role=hammer-worker >/dev/null
|
|
ip=$(hcloud server ip "$name")
|
|
echo "$name $ip" >> "$FLEET"
|
|
echo " ip=$ip; esperando SSH…"
|
|
until $SSH root@"$ip" true 2>/dev/null; do sleep 3; done
|
|
|
|
echo " alineando cola con el laptop (rsync código + recipes)…"
|
|
# ── EL MANIFIESTO VIAJA ACÁ TAMBIÉN, Y ESTE ERA EL AGUJERO ────────────────────────────────────
|
|
# `farm-sync.sh` ya mandaba `work/farm-sellados.txt` para que el worker no rehiciera lo que el hub
|
|
# tiene. Este fichero NO, así que un worker recién creado arrancaba con el manifiesto RANCIO que
|
|
# venía horneado en la golden. Medido el 2026-08-09: worker 1172 vs hub 1241 ⇒ el worker construía
|
|
# `vulkan-loader` en TODOS los ciclos aunque el hub ya lo tenía sellado, y encima fallaba.
|
|
#
|
|
# Es literalmente el error de método que este repo ya se documentó a sí mismo en `farm-sync.sh`:
|
|
# «lo puse allí, di el bucle por cerrado, y la churn siguió porque hay DOS rutas». Había dos rutas
|
|
# otra vez —`farm-sync` y `farm-up`— y sólo una estaba arreglada. Si aparece una tercera, va con
|
|
# el mismo include.
|
|
# ⚠ `--exclude /work` NO excluía el contenido, sólo la entrada. Con `--include '/work/'` delante,
|
|
# rsync entraba al directorio y sus hijos no casaban con ninguna regla ⇒ **se copiaba `work/`
|
|
# entero**. Medido el 2026-08-11 montando el primer worker con el lab anclado: llegaron 3,7 G de
|
|
# `work/`, incluidos los mirrors de git A MEDIO COPIAR (el rsync abortó antes de terminarlos), y
|
|
# el worker fallaba cada receta con `git fetch … exit 128`. Encima, con `--delete`, moría con
|
|
# «cannot delete non-empty directory» sobre los `vendor/` de cargo, que quedan de SÓLO LECTURA.
|
|
# `/work/**` sí ancla el contenido. Un `work/` a medias es peor que ninguno: parece que está.
|
|
set +e
|
|
rsync -az --delete -e "$SSH" \
|
|
--include '/work/' --include '/work/farm-sellados.txt' --exclude '/work/**' \
|
|
--exclude /store --exclude '/store-*' --exclude /target \
|
|
--exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \
|
|
--exclude '*.png' --exclude '/content*' \
|
|
./ "root@$ip:$REMOTE/"
|
|
rc=$?
|
|
set -e
|
|
# NO abortamos si el rsync falla. Lo que viene después es el ARMADO DEL DEAD-MAN, y un `set -e`
|
|
# aquí dejaba un server VIVO SIN poder autodestruirse — que es justo lo que este script declara
|
|
# inadmisible. El orden ideal sería armar el dead-man ANTES de cualquier paso que pueda fallar;
|
|
# mientras eso no se reordene, esto garantiza al menos que se llegue a armarlo.
|
|
[ "$rc" -ne 0 ] && echo " ⚠ rsync de código salió $rc — el worker puede tener la cola incompleta; sigo para ARMAR EL DEAD-MAN"
|
|
# el servicio ya arrancó en el boot (baked/enabled); reiniciar fuerza rescan inmediato de la cola
|
|
$SSH root@"$ip" 'systemctl restart hammer-farm' 2>/dev/null || true
|
|
|
|
# VOLUMEN PERSISTENTE + STORE DENTRO. Sin esto la automuerte perdería lo construido: hworker-4
|
|
# tenía 438 artefactos (37G) SÓLO en su disco local. El store vive en /mnt/cosecha/store ⇒ cada
|
|
# artefacto está en el volumen persistente EN EL INSTANTE en que se sella. "Guardar lo generado"
|
|
# deja de ser un paso que puede fallar antes de morir y pasa a ser la estructura. El volumen
|
|
# sobrevive al server; se cosecha con harkaq-vol.sh collect y se clausura con close.
|
|
echo " montando volumen persistente $VOL_NAME y anclando el store dentro…"
|
|
vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}')
|
|
if [ -z "$vid" ]; then
|
|
hcloud volume create --name "$VOL_NAME" --size "$VOL_SIZE" --location "$LOCATION" --format ext4 >/dev/null 2>&1 || true
|
|
vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}')
|
|
fi
|
|
if [ -n "$vid" ]; then
|
|
# UN VOLUMEN SE ADJUNTA A UN SERVER, y este script crea N workers en un bucle apuntando todos al
|
|
# MISMO VOL_NAME ⇒ del segundo en adelante el attach no tiene nada que adjuntar. Antes ese fallo
|
|
# se perdía dos veces: `>/dev/null 2>&1` escondía el mensaje y `|| true` el código de salida.
|
|
# Se pregunta ANTES, y el motivo se dice con el nombre del server que lo tiene tomado.
|
|
dueno=$(hcloud volume list -o noheader -o columns=name,server 2>/dev/null | awk -v v="$VOL_NAME" '$1==v{print $2}')
|
|
if [ -n "$dueno" ] && [ "$dueno" != "-" ] && [ "$dueno" != "$name" ]; then
|
|
sin_volumen "$name" "el volumen $VOL_NAME ya está adjunto a $dueno (un volumen = un server)"
|
|
fi
|
|
if ! err=$(hcloud volume attach --server "$name" "$VOL_NAME" 2>&1); then
|
|
sin_volumen "$name" "no pude adjuntar $VOL_NAME: ${err:-error desconocido}"
|
|
fi
|
|
dev="/dev/disk/by-id/scsi-0HC_Volume_$vid"
|
|
$SSH root@"$ip" "mkdir -p /mnt/cosecha
|
|
# ⚠ El mkfs.ext4 -F de rescate SÓLO si el dispositivo no tiene filesystem. La versión previa
|
|
# formateaba ante CUALQUIER fallo de montaje —incluida la carrera normal entre volume attach
|
|
# y la aparición del dispositivo— o sea que un hipo de segundos destruía el volumen entero.
|
|
# blkid distingue «vacío» de «no pude montarlo ahora».
|
|
if ! mount $dev /mnt/cosecha 2>/dev/null; then
|
|
if blkid $dev >/dev/null 2>&1; then
|
|
echo " ✗ el volumen TIENE filesystem y no montó — NO lo formateo. Revisá a mano." >&2; exit 1
|
|
fi
|
|
mkfs.ext4 -q -F $dev && mount $dev /mnt/cosecha
|
|
fi
|
|
mkdir -p /mnt/cosecha/store /mnt/cosecha/verdicts /mnt/cosecha/logs
|
|
# el store del worker ES el del volumen: sellar YA es persistir.
|
|
# PERO antes hay que RESCATAR el store HORNEADO en la golden (2026-07-21): esto hacía
|
|
# \`rm -rf $REMOTE/store\` y tiraba el catálogo cacheado que es la razón de ser de la imagen
|
|
# golden ('arranca con TODO el catálogo ⇒ cache-hit instantáneo'). Sin él, el worker mide
|
|
# deuda contra un store casi vacío y se pone a reconstruir media distro: en la tanda del
|
|
# 2026-07-21 dio 716 recetas de deuda donde el hub medía 37. Es CAS (nombre = hash) ⇒
|
|
# fusionar es seguro por construcción: \`mv -n\` no pisa lo que el volumen ya tiene.
|
|
# ⚠ mountpoint -q NO ES OPCIONAL — sin él este bloque BORRA EL VOLUMEN.
|
|
# La guarda original sólo comprobaba «es directorio y no es symlink». Eso basta en un server
|
|
# recién creado desde la golden ORIGINAL, donde /opt/hammer/store es un directorio normal.
|
|
# Pero al re-snapshotear un worker (2026-08-08) la imagen se llevó también la CONFIGURACIÓN DE
|
|
# MONTAJE: el server nuevo monta el volumen en /opt/hammer/store al arrancar, ANTES de que
|
|
# corra este rescate. Entonces rm -rf $REMOTE/store borra A TRAVÉS DEL MONTAJE y se lleva el
|
|
# store del volumen entero — que es justo lo que el volumen existe para proteger.
|
|
# Medido: el volumen pasó de ~900 artefactos a 2. No se perdió el filesystem (lost+found es de
|
|
# julio, 17 montajes): se borró el CONTENIDO.
|
|
# ⇒ Si ya es punto de montaje, no hay nada que rescatar ni que borrar: el store ya está donde
|
|
# tiene que estar.
|
|
if [ -d $REMOTE/store ] && [ ! -L $REMOTE/store ] && ! mountpoint -q $REMOTE/store; then
|
|
n=\$(ls $REMOTE/store 2>/dev/null | wc -l)
|
|
[ \"\$n\" -gt 0 ] && mv -n $REMOTE/store/* /mnt/cosecha/store/ 2>/dev/null
|
|
echo \" ✓ rescatados \$n artefactos horneados de la golden al volumen\"
|
|
rm -rf $REMOTE/store
|
|
fi
|
|
# BIND-MOUNT, NO SYMLINK (2026-07-22). Anclar el store con \`ln -s\` rompía TODA receta con
|
|
# \`zig_version\` en el worker, y el síntoma se leía como deuda de la cascada GUI. Por qué:
|
|
# hammer deriva el lab del PADRE DEL STORE (BuildConfig::defaults_for_store ⇒
|
|
# project_root = store_root.parent(), de ahí .dev-fs/{alpine,tools/zig,cache}). Con el symlink
|
|
# el store resuelve a /mnt/cosecha/store ⇒ busca /mnt/cosecha/.dev-fs, que no existe, y muere
|
|
# con 'no encuentro el ejecutable zig en …' apuntando al lugar equivocado. Un bind-mount deja
|
|
# los datos en el volumen igual, pero \$REMOTE/store sigue siendo una ruta REAL bajo
|
|
# \$REMOTE ⇒ el padre vuelve a ser /opt/hammer y .dev-fs se encuentra. Mantiene las dos
|
|
# invariantes a la vez: sellar = persistir, y el lab donde hammer lo espera.
|
|
if [ -L $REMOTE/store ]; then rm -f $REMOTE/store; fi
|
|
mkdir -p $REMOTE/store
|
|
mountpoint -q $REMOTE/store || mount --bind /mnt/cosecha/store $REMOTE/store
|
|
grep -q /mnt/cosecha /etc/fstab || echo '$dev /mnt/cosecha ext4 defaults,nofail 0 0' >> /etc/fstab
|
|
grep -q \"$REMOTE/store\" /etc/fstab || echo '/mnt/cosecha/store $REMOTE/store none bind,nofail 0 0' >> /etc/fstab" >/dev/null 2>&1
|
|
if $SSH root@"$ip" 'mountpoint -q /mnt/cosecha && mountpoint -q '"$REMOTE"'/store' 2>/dev/null; then
|
|
echo " ✓ store anclado al volumen ($( $SSH root@"$ip" 'ls -d /mnt/cosecha/store/*/ 2>/dev/null | wc -l') artefactos ya presentes)"
|
|
else
|
|
sin_volumen "$name" "el store NO quedó anclado al volumen (mountpoint dice que no)"
|
|
fi
|
|
else
|
|
sin_volumen "$name" "no existe el volumen $VOL_NAME y no pude crearlo"
|
|
fi
|
|
|
|
# DEAD-MAN SWITCH: el worker se borra SOLO tras 1h sin trabajo. Va acá, en el nacimiento, porque
|
|
# el modelo "efímero" NO puede depender de que alguien llame a farm-down: hworker-4 estuvo 5 días
|
|
# idle (2026-07-17) justo por eso — el agente que lo creó se fue y el server quedó. Un invariante
|
|
# que necesita que alguien se acuerde no es un invariante.
|
|
echo " armando dead-man switch (1h idle ⇒ auto-delete)…"
|
|
tok="${HCLOUD_TOKEN:-$(grep -shoE '^ *token *= *"?[A-Za-z0-9]+' "$HOME/.config/hcloud/cli.toml" 2>/dev/null | head -1 | sed 's/.*[= "]//')}"
|
|
if [ -z "$tok" ]; then
|
|
echo " ⚠ SIN TOKEN para el dead-man switch: este worker NO se auto-borrará."
|
|
echo " (un poweroff no sirve: Hetzner cobra igual con el server apagado)"
|
|
else
|
|
scp -q -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
scripts/farm/deadman.sh "root@$ip:/usr/local/bin/deadman.sh" 2>/dev/null
|
|
scp -q -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
scripts/farm/hammer-deadman.service scripts/farm/hammer-deadman.timer \
|
|
"root@$ip:/etc/systemd/system/" 2>/dev/null
|
|
$SSH root@"$ip" "chmod +x /usr/local/bin/deadman.sh
|
|
printf 'HCLOUD_TOKEN=%s\n' '$tok' > /etc/hammer-deadman.env; chmod 600 /etc/hammer-deadman.env
|
|
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).
|
|
# 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 " ⛔ $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"
|
|
done
|
|
|
|
sort -u -o "$FLEET" "$FLEET"
|
|
echo "==> flota viva:"; cat "$FLEET"
|
|
echo "cosechar+destruir con: scripts/farm/farm-down.sh"
|