tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ c21602b7070445a7c10f6a3da56741723b0cdfec scripts/farm/farm-up.sh Shell · 141 lines · 8.7 KB History raw

farm: fix — farm-up tiraba el store HORNEADO de la golden (rm -rf) y el worker medía media distro como deuda c21602b70 · sergio ·

1#!/usr/bin/env bash
2# farm-up.sh [N] — levanta N workers EFÍMEROS desde el snapshot golden y los alinea con la cola
3# actual del laptop. Modelo hub-and-spoke: el worker NO tiene secretos (ni firma ni gitea); es
4# compute puro y descartable. La imagen 'hammer-golden' trae el toolchain + el store sellado
5# horneados ⇒ el worker arranca con TODO el catálogo cacheado (cache-hit instantáneo), sólo
6# construye lo NUEVO que el hub le empuje.
7#
8# Flujo:  scripts/farm/farm-up.sh 3      # crea hworker-1..3, los alimenta con la cola actual
9#         ... (muelen) ...
10#         scripts/farm/farm-down.sh      # cosecha su store al laptop y los DESTRUYE
11#
12# La flota viva se registra en scripts/farm/.fleet ("<name> <ip>" por línea; gitignored).
13# Idempotente al escalar: re-correr con más N agrega workers sin pisar los vivos.
14#
15# Env: IMAGE (snapshot, def 408909310=hammer-golden-harkaq-6.17-2026-07-15), TYPE (def ccx23),
16#      LOCATION
17#      (def hel1), SSHKEY (llave del proyecto Hetzner, def desarrollo@jlsoltech.com), SSH_KEY
18#      (privada local p/ SSH, def ~/.ssh/github5), REMOTE (def /opt/hammer).
19set -euo pipefail
20
21N="${1:-1}"
22IMAGE="${IMAGE:-408909310}"
23TYPE="${TYPE:-ccx23}"
24LOCATION="${LOCATION:-hel1}"
25SSHKEY="${SSHKEY:-desarrollo@jlsoltech.com}"
26SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
27REMOTE="${REMOTE:-/opt/hammer}"
28# MISMO volumen que harkaq-vol.sh, a propósito: eran DOS caminos de crear workers y sólo uno tenía
29# volumen+automuerte. hworker-4 nació por ÉSTE (farm-up) y por eso quedó 5 días idle con 37G que
30# nadie salvó — la protección existía y este camino la esquivaba. Compartir el volumen hace que
31# `harkaq-vol.sh collect` y `close` cosechen y clausuren lo de AMBOS caminos.
32VOL_NAME="${VOL_NAME:-harkaq-cosecha}"
33VOL_SIZE="${VOL_SIZE:-100}"                  # GB. ~€0.044/GB/mes ⇒ 100G ≈ €4.4/mes MIENTRAS exista.
34                                             # El baseline €0 lo da `harkaq-vol.sh close`, que es
35                                             # tuyo: el volumen es el único que sobrevive a propósito.
36ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
37FLEET="$ROOT/scripts/farm/.fleet"
38# Workers efímeros RECICLAN IPs de Hetzner ⇒ un known_hosts persistente choca ("HOST KEY CHANGED")
39# y accept-new RECHAZA la clave cambiada, colgando el `until ssh true` para siempre. Como el modelo
40# es hub-and-spoke (el worker NO tiene secretos: compute puro descartable), saltamos la verificación
41# de host y no ensuciamos known_hosts (/dev/null). No hay superficie MITM que proteger acá.
42SSH="ssh -i $SSH_KEY -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR -o ConnectTimeout=10"
43cd "$ROOT"
44touch "$FLEET"
45
46# índice de arranque: el mayor hworker-N vivo + 1 (para escalar sin colisión de nombres)
47base=$(sed -n 's/^hworker-\([0-9]*\) .*/\1/p' "$FLEET" | sort -n | tail -1)
48base="${base:-0}"
49
50for k in $(seq 1 "$N"); do
51  i=$((base + k)); name="hworker-$i"
52  echo "==> creando $name ($TYPE @ $LOCATION desde image $IMAGE)"
53  hcloud server create --name "$name" --image "$IMAGE" --type "$TYPE" \
54    --location "$LOCATION" --ssh-key "$SSHKEY" --label role=hammer-worker >/dev/null
55  ip=$(hcloud server ip "$name")
56  echo "$name $ip" >> "$FLEET"
57  echo "   ip=$ip; esperando SSH…"
58  until $SSH root@"$ip" true 2>/dev/null; do sleep 3; done
59
60  echo "   alineando cola con el laptop (rsync código + recipes)…"
61  rsync -az --delete -e "$SSH" \
62    --exclude /work --exclude /store --exclude '/store-*' --exclude /target \
63    --exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \
64    --exclude '*.png' --exclude '/content*' \
65    ./ "root@$ip:$REMOTE/"
66  # el servicio ya arrancó en el boot (baked/enabled); reiniciar fuerza rescan inmediato de la cola
67  $SSH root@"$ip" 'systemctl restart hammer-farm' 2>/dev/null || true
68
69  # VOLUMEN PERSISTENTE + STORE DENTRO. Sin esto la automuerte perdería lo construido: hworker-4
70  # tenía 438 artefactos (37G) SÓLO en su disco local. El store vive en /mnt/cosecha/store ⇒ cada
71  # artefacto está en el volumen persistente EN EL INSTANTE en que se sella. "Guardar lo generado"
72  # deja de ser un paso que puede fallar antes de morir y pasa a ser la estructura. El volumen
73  # sobrevive al server; se cosecha con harkaq-vol.sh collect y se clausura con close.
74  echo "   montando volumen persistente $VOL_NAME y anclando el store dentro…"
75  vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}')
76  if [ -z "$vid" ]; then
77    hcloud volume create --name "$VOL_NAME" --size "$VOL_SIZE" --location "$LOCATION" --format ext4 >/dev/null 2>&1 || true
78    vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}')
79  fi
80  if [ -n "$vid" ]; then
81    hcloud volume attach --server "$name" "$VOL_NAME" >/dev/null 2>&1 || true
82    dev="/dev/disk/by-id/scsi-0HC_Volume_$vid"
83    $SSH root@"$ip" "mkdir -p /mnt/cosecha
84      mount $dev /mnt/cosecha 2>/dev/null || { mkfs.ext4 -q -F $dev && mount $dev /mnt/cosecha; }
85      mkdir -p /mnt/cosecha/store /mnt/cosecha/verdicts /mnt/cosecha/logs
86      # el store del worker ES el del volumen: sellar YA es persistir.
87      # PERO antes hay que RESCATAR el store HORNEADO en la golden (2026-07-21): esto hacía
88      # \`rm -rf $REMOTE/store\` y tiraba el catálogo cacheado que es la razón de ser de la imagen
89      # golden ('arranca con TODO el catálogo ⇒ cache-hit instantáneo'). Sin él, el worker mide
90      # deuda contra un store casi vacío y se pone a reconstruir media distro: en la tanda del
91      # 2026-07-21 dio 716 recetas de deuda donde el hub medía 37. Es CAS (nombre = hash) ⇒
92      # fusionar es seguro por construcción: \`mv -n\` no pisa lo que el volumen ya tiene.
93      if [ -d $REMOTE/store ] && [ ! -L $REMOTE/store ]; then
94        n=\$(ls $REMOTE/store 2>/dev/null | wc -l)
95        [ \"\$n\" -gt 0 ] && mv -n $REMOTE/store/* /mnt/cosecha/store/ 2>/dev/null
96        echo \"   ✓ rescatados \$n artefactos horneados de la golden al volumen\"
97        rm -rf $REMOTE/store
98      fi
99      ln -sfn /mnt/cosecha/store $REMOTE/store
100      grep -q /mnt/cosecha /etc/fstab || echo '$dev /mnt/cosecha ext4 defaults,nofail 0 0' >> /etc/fstab" >/dev/null 2>&1
101    if $SSH root@"$ip" 'mountpoint -q /mnt/cosecha && test -L '"$REMOTE"'/store' 2>/dev/null; then
102      echo "   ✓ store anclado al volumen ($(  $SSH root@"$ip" 'ls -d /mnt/cosecha/store/*/ 2>/dev/null | wc -l') artefactos ya presentes)"
103    else
104      echo "   ⚠ el store NO quedó en el volumen ⇒ lo que construya se PIERDE al auto-borrarse. Revisar."
105    fi
106  else
107    echo "   ⚠ sin volumen ⇒ este worker construiría a disco local y la automuerte perdería todo."
108  fi
109
110  # DEAD-MAN SWITCH: el worker se borra SOLO tras 1h sin trabajo. Va acá, en el nacimiento, porque
111  # el modelo "efímero" NO puede depender de que alguien llame a farm-down: hworker-4 estuvo 5 días
112  # idle (2026-07-17) justo por eso — el agente que lo creó se fue y el server quedó. Un invariante
113  # que necesita que alguien se acuerde no es un invariante.
114  echo "   armando dead-man switch (1h idle ⇒ auto-delete)…"
115  tok="${HCLOUD_TOKEN:-$(grep -shoE '^ *token *= *"?[A-Za-z0-9]+' "$HOME/.config/hcloud/cli.toml" 2>/dev/null | head -1 | sed 's/.*[= "]//')}"
116  if [ -z "$tok" ]; then
117    echo "   ⚠ SIN TOKEN para el dead-man switch: este worker NO se auto-borrará."
118    echo "     (un poweroff no sirve: Hetzner cobra igual con el server apagado)"
119  else
120    scp -q -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
121      scripts/farm/deadman.sh "root@$ip:/usr/local/bin/deadman.sh" 2>/dev/null
122    scp -q -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
123      scripts/farm/hammer-deadman.service scripts/farm/hammer-deadman.timer \
124      "root@$ip:/etc/systemd/system/" 2>/dev/null
125    $SSH root@"$ip" "chmod +x /usr/local/bin/deadman.sh
126      printf 'HCLOUD_TOKEN=%s\n' '$tok' > /etc/hammer-deadman.env; chmod 600 /etc/hammer-deadman.env
127      systemctl daemon-reload && systemctl enable --now hammer-deadman.timer" >/dev/null 2>&1
128    # EVIDENCIA, no fe: que el timer esté ARMADO se comprueba, no se asume (el cron de aquella vez
129    # estaba "validado" y nunca disparó porque el PID 1 no tenía crond).
130    if $SSH root@"$ip" 'systemctl is-active hammer-deadman.timer' 2>/dev/null | grep -q active; then
131      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}')"
132    else
133      echo "   ⚠ el timer NO quedó activo ⇒ este worker puede quedarse idle para siempre. Revisar."
134    fi
135  fi
136  echo "   ✓ $name muele la cola actual"
137done
138
139sort -u -o "$FLEET" "$FLEET"
140echo "==> flota viva:"; cat "$FLEET"
141echo "cosechar+destruir con: scripts/farm/farm-down.sh"