diff --git a/.gitignore b/.gitignore index da572db0..f729e667 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ __pycache__/ # Scratch para bootstrap del frente rust (mrustc clone + fuentes rustc, multi-GB) .scratch/ +scripts/farm/.fleet diff --git a/scripts/farm/farm-down.sh b/scripts/farm/farm-down.sh new file mode 100755 index 00000000..6ba0d013 --- /dev/null +++ b/scripts/farm/farm-down.sh @@ -0,0 +1,52 @@ +#!/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" +SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new -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 + echo "==> cosechando store de $name ($ip)" + if rsync -az -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 + echo "==> promote+firma final (cache-hit en lo cosechado)" + scripts/build-farm.sh || echo " (build-farm devolvió error; revisá logs)" +fi diff --git a/scripts/farm/farm-up.sh b/scripts/farm/farm-up.sh new file mode 100755 index 00000000..5d3f3244 --- /dev/null +++ b/scripts/farm/farm-up.sh @@ -0,0 +1,60 @@ +#!/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 (" " por línea; gitignored). +# Idempotente al escalar: re-correr con más N agrega workers sin pisar los vivos. +# +# Env: IMAGE (snapshot, def 405120842=hammer-golden-2026-07-05), 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}" +IMAGE="${IMAGE:-405120842}" +TYPE="${TYPE:-ccx23}" +LOCATION="${LOCATION:-hel1}" +SSHKEY="${SSHKEY:-desarrollo@jlsoltech.com}" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" +REMOTE="${REMOTE:-/opt/hammer}" +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +FLEET="$ROOT/scripts/farm/.fleet" +SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" +cd "$ROOT" +touch "$FLEET" + +# í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)…" + rsync -az --delete -e "$SSH" \ + --exclude /work --exclude /store --exclude '/store-*' --exclude /target \ + --exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \ + --exclude '*.png' --exclude '/content*' \ + ./ "root@$ip:$REMOTE/" + # 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 + 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"