#!/usr/bin/env bash # farm-down.sh [name...] — COSECHA el store sellado de cada worker efímero al laptop y luego lo # DESTRUYE. Sin args opera sobre toda la flota registrada en scripts/farm/.fleet. Con args, sólo # sobre los workers nombrados (para bajar la escala parcialmente). # # El orden importa: primero rsync-eamos el store (CAS, merge seguro con el del laptop), y SÓLO si # el pull salió bien destruimos el server. Tras destruir todo, promovemos+firmamos lo cosechado # (cache-hit en los artefactos recién bajados) — el store del laptop es la fuente canónica. # # Uso: scripts/farm/farm-down.sh # cosecha + destruye toda la flota # scripts/farm/farm-down.sh hworker-2 # sólo ese # Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer), NO_PROMOTE=1 (sólo cosecha+destruye) set -uo pipefail SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" REMOTE="${REMOTE:-/opt/hammer}" ROOT="$(cd "$(dirname "$0")/../.." && pwd)" FLEET="$ROOT/scripts/farm/.fleet" # Ver nota en farm-up.sh: workers efímeros reciclan IPs ⇒ known_hosts a /dev/null (hub-and-spoke, # worker sin secretos, sin superficie MITM que proteger). SSH="ssh -i $SSH_KEY -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR -o ConnectTimeout=10" cd "$ROOT" [ -s "$FLEET" ] || { echo "flota vacía (scripts/farm/.fleet)"; exit 0; } want=" $* " # si hay args, filtrar por nombre; si no, todos harvested=0 newfleet="$(mktemp)" while read -r name ip; do [ -n "${name:-}" ] || continue if [ "$#" -gt 0 ] && [ "${want#* $name }" = "$want" ]; then echo "$name $ip" >> "$newfleet"; continue # no seleccionado: sigue vivo fi # ── BLINDAJE (2026-07-17, petición explícita del usuario): gioser.net es FIJO, vive en el MISMO # proyecto hcloud y NO TIENE BACKUP. Este script borra por NOMBRE leído de .fleet, sin verificar # nada: si un nombre equivocado entra a esa lista (a mano, por un bug, por un rsync), lo destruye # sin preguntar. Dos capas iguales a las del deadman — la del label es la fuerte, porque no # depende de mantener una lista al día. gioser no tiene labels (map[]) ⇒ nunca pasa. case "$name" in gioser|gioser.net|*gioser*) echo " ⛔ '$name' está en la LISTA NEGRA (server fijo sin backup) — NO se toca" echo "$name $ip" >> "$newfleet"; continue ;; esac if ! hcloud server describe "$name" -o format='{{.Labels}}' 2>/dev/null | grep -q hammer-worker; then echo " ⛔ '$name' NO tiene label role=hammer-worker ⇒ no nació de la granja. NO se borra." echo "$name $ip" >> "$newfleet"; continue fi echo "==> cosechando store de $name ($ip)" # ⚠ `--exclude-from` del registro de podados: el worker NO se poda, así que devolvería los # artefactos superados y desharía la poda (24 G por vuelta). Misma protección que en # `cosecha-cron.sh`, `farm-sync.sh` y `store-gc.sh` — son CUATRO rutas que bajan el store del # worker y la protección tiene que estar en las cuatro. Se descubrió tras arreglar sólo una y # comprobar que la churn seguía. LEDGER_GC="$ROOT/work/store-gc-superados.txt" EXCL_GC=""; [ -s "$LEDGER_GC" ] && EXCL_GC="--exclude-from=$LEDGER_GC" if rsync -az $EXCL_GC -e "$SSH" "root@$ip:$REMOTE/store/" "$ROOT/store/"; then echo "==> destruyendo $name" if hcloud server delete "$name" >/dev/null 2>&1; then harvested=$((harvested + 1)) else echo " ⚠ no pude destruir $name — lo dejo en la flota"; echo "$name $ip" >> "$newfleet" fi else echo " ⚠ falló el pull de store de $name — NO lo destruyo (queda en la flota)" echo "$name $ip" >> "$newfleet" fi done < "$FLEET" mv "$newfleet" "$FLEET" echo "==> cosechados+destruidos: $harvested; flota restante:"; cat "$FLEET" if [ "${NO_PROMOTE:-}" != "1" ] && [ "$harvested" -gt 0 ]; then # El worker muele TODAS las colas (ver QUEUES en farm-worker-loop.sh); build-farm.sh sin QUEUE # sólo mira recipes/incoming ⇒ los artefactos de incoming-go/incoming-clib se cosechaban pero # NO se promovían/firmaban. Recorrer las mismas colas que el worker (cache-hit en lo cosechado). # El guard `ls "$Q"/*.toml` salta las colas vacías, así que listar todas es inocuo. echo "==> promote+firma final (cache-hit en lo cosechado)" for Q in ${QUEUES:-recipes/incoming recipes/incoming-go recipes/incoming-clib}; do ls "$Q"/*.toml >/dev/null 2>&1 || continue echo " --- promoviendo $Q" QUEUE="$Q" scripts/build-farm.sh || echo " (build-farm $Q devolvió error; revisá logs)" done fi