Etapa G: kit de granja distribuida (worker VPS hub-and-spoke)

Paraleliza el build a un/varios VPS sin exponer gitea ni la clave de firma.
Modelo: el laptop es el HUB (firma + gitea); el worker sólo construye la cola
incoming/ y sella al store local (PROMOTE=0). Reproducibilidad bit-a-bit +
content-addressing ⇒ el store del worker es byte-idéntico, se rsync-ea de vuelta
y el hash valida solo (no hay que confiar en el VPS).

- vps-setup.sh: provisiona Debian/Ubuntu (deps, userns p/bwrap, swap 16G,
  rustup, build hammer, bootstrap-devfs rootfs Alpine edge, systemd unit).
- farm-worker-loop.sh: loop autónomo 24/7, PROMOTE=0, watchdog de disco
  integrado; muele lo que haya sin esperar al hub.
- hammer-farm.service: systemd, JOBS=2 (CCX13 = 2 vCPU dedicado), Nice/idle-io.
- farm-sync.sh (hub): rsync código+cola arriba, store sellado abajo,
  promote+firma+commit+push. Acceso único laptop->VPS por SSH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 03:50:43 -04:00
co-authored by Claude Opus 4.8
parent 97118483d0
commit d949d10dcd
4 changed files with 167 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# farm-sync.sh <user@host> — orquesta el worker VPS desde el HUB (laptop). Una sola pasada:
# 1. sube código + recipes/incoming/ al worker (rsync, el worker no toca gitea)
# 2. baja el store/ sellado que el worker construyó
# 3. promueve+firma lo cosechado (cache-hit instantáneo en los artefactos bajados) + commit/push
#
# El acceso es UNO solo: laptop->VPS por SSH (la clave de firma y gitea nunca salen del laptop).
# Idempotente y seguro de re-correr. Pensado para correr cada vez que el laptop está online.
#
# Uso: scripts/farm/farm-sync.sh root@1.2.3.4
# scripts/farm/farm-sync.sh root@1.2.3.4 --no-promote # sólo sincroniza, no firma
# Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer)
set -euo pipefail
VPS="${1:?uso: farm-sync.sh user@host [--no-promote]}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
REMOTE="${REMOTE:-/opt/hammer}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
echo "==> 1. subiendo código + cola al worker ($VPS:$REMOTE)"
rsync -az --delete -e "$SSH" \
--exclude /work --exclude /store --exclude '/store-*' --exclude /target \
--exclude /dist --exclude /.dev-fs --exclude /.git \
./ "$VPS:$REMOTE/"
echo "==> 2. bajando artefactos sellados del worker"
rsync -az -e "$SSH" "$VPS:$REMOTE/store/" ./store/
if [ "${2:-}" = "--no-promote" ]; then
echo "✓ store sincronizado (sin promote)"; exit 0
fi
echo "==> 3. promote + firma (cache-hit en lo que el worker selló)"
scripts/build-farm.sh
echo "==> 4. commit + push de la cosecha"
git add recipes/ tandas/ 2>/dev/null || true
if ! git diff --cached --quiet; then
git commit -q -m "Etapa G: cosecha del worker VPS — promote + firma"
git push -q origin main && echo "✓ pusheado"
else
echo "(nada nuevo que commitear)"
fi
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# farm-worker-loop.sh — loop AUTÓNOMO del worker. Construye la cola recipes/incoming/ en
# bucle (PROMOTE=0: sólo sella al store local, NO promueve, NO firma, NO toca gitea), con
# watchdog de disco integrado. El hub (laptop) le mete recetas nuevas vía farm-sync.sh y le
# baja el store sellado; este loop sólo muele lo que haya, 24/7, sin esperar a nadie.
#
# Idempotente: cada ciclo re-escanea incoming/; lo ya-sellado sale por cache-hit (instantáneo),
# sólo construye lo nuevo. Cuando la cola está toda construida, duerme IDLE_SLEEP y reintenta.
#
# Lo lanza systemd (hammer-farm.service). Manual: JOBS=2 ./scripts/farm/farm-worker-loop.sh
set -uo pipefail
HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
cd "$HAMMER_DIR"
. "$HOME/.cargo/env" 2>/dev/null || true
JOBS="${JOBS:-$(nproc)}"
IDLE_SLEEP="${IDLE_SLEEP:-300}"
# --- watchdog de disco: cada 3 min borra work/sources/* que ningún bwrap activo bind-monta
# (seguro: sellada=store CAS, en-cola=se re-extrae sola). Evita que el vendoring llene el disco.
(
while :; do
sleep 180
active=$(pgrep -af bwrap 2>/dev/null | grep -oE 'work/sources/[^ ]+' | sort -u)
for d in work/sources/*/; do
[ -d "$d" ] || continue; dd=${d%/}
printf '%s\n' "$active" | grep -qxF "$dd" && continue
rm -rf "$dd"
done
done
) &
echo "$(date -u +%FT%TZ) worker-loop arrancado: JOBS=$JOBS IDLE_SLEEP=$IDLE_SLEEP"
while :; do
n=$(ls recipes/incoming/*.toml 2>/dev/null | wc -l)
if [ "$n" -gt 0 ]; then
echo "$(date -u +%FT%TZ) ciclo: $n recetas en cola"
JOBS="$JOBS" PROMOTE=0 scripts/build-farm.sh 2>&1 | tail -50
echo "$(date -u +%FT%TZ) ciclo terminado; store sellado disponible para el hub"
else
echo "$(date -u +%FT%TZ) cola vacía; durmiendo ${IDLE_SLEEP}s"
fi
sleep "$IDLE_SLEEP"
done
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=hammer build farm worker (loop autónomo)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/hammer
Environment=HOME=/root
Environment=HAMMER_DIR=/opt/hammer
# JOBS = vCPU (cada build usa 1 core por codegen-units=1). CCX13 = 2.
Environment=JOBS=2
ExecStart=/opt/hammer/scripts/farm/farm-worker-loop.sh
Restart=always
RestartSec=30
# deja respirar al sistema en una caja chica
Nice=10
IOSchedulingClass=idle
[Install]
WantedBy=multi-user.target
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# vps-setup.sh — provisiona un WORKER de granja hammer en un VPS Debian/Ubuntu fresco.
#
# Modelo hub-and-spoke: el LAPTOP es el hub (tiene la clave de firma + gitea). El worker
# NO toca gitea ni firma nada — sólo construye la cola `recipes/incoming/` y sella al store
# local (PROMOTE=0). El hub le rsync-ea código+cola y le baja el store con `farm-sync.sh`.
#
# El hub corre esto por SSH tras rsync-ear el repo a $HAMMER_DIR. Idempotente (re-correr
# actualiza in-place). Necesita root (apt + sysctl + swap + systemd).
#
# Uso (en el VPS, como root): HAMMER_DIR=/opt/hammer ./scripts/farm/vps-setup.sh
set -euo pipefail
HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
cd "$HAMMER_DIR"
echo "==> 1. deps del sistema (apt)"
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y -q git build-essential curl pkg-config libssl-dev \
bubblewrap rsync ca-certificates uidmap xz-utils
echo "==> 2. user namespaces para bwrap (Debian/Ubuntu los capan por defecto)"
sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true
cat > /etc/sysctl.d/99-hammer-userns.conf <<'EOF'
kernel.unprivileged_userns_clone=1
kernel.apparmor_restrict_unprivileged_userns=0
EOF
sysctl --system >/dev/null 2>&1 || true
echo "==> 3. swap 16G (8GB de RAM es justo para builds Rust con JOBS=2)"
if ! swapon --show 2>/dev/null | grep -q /swapfile; then
fallocate -l 16G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=16384 status=none
chmod 600 /swapfile; mkswap /swapfile >/dev/null; swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
echo "==> 4. rust host toolchain (para buildear el binario hammer)"
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --profile minimal --default-toolchain stable
fi
. "$HOME/.cargo/env"
echo "==> 5. build hammer (target/release/hammer)"
cargo build --release --bin hammer
echo "==> 6. rootfs Alpine edge + zig (bootstrap reproducible; varios min la 1ra vez)"
./scripts/bootstrap-devfs.sh
echo "==> 7. systemd unit del loop"
cp scripts/farm/hammer-farm.service /etc/systemd/system/hammer-farm.service
systemctl daemon-reload
echo "✓ worker provisionado."
echo " arrancar: systemctl enable --now hammer-farm"
echo " logs: journalctl -u hammer-farm -f"