From d949d10dcdf152ab6649fc647e0af525ac45fcc5 Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 24 Jun 2026 03:50:43 -0400 Subject: [PATCH] Etapa G: kit de granja distribuida (worker VPS hub-and-spoke) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/farm/farm-sync.sh | 45 +++++++++++++++++++++++++ scripts/farm/farm-worker-loop.sh | 43 +++++++++++++++++++++++ scripts/farm/hammer-farm.service | 21 ++++++++++++ scripts/farm/vps-setup.sh | 58 ++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100755 scripts/farm/farm-sync.sh create mode 100755 scripts/farm/farm-worker-loop.sh create mode 100644 scripts/farm/hammer-farm.service create mode 100755 scripts/farm/vps-setup.sh diff --git a/scripts/farm/farm-sync.sh b/scripts/farm/farm-sync.sh new file mode 100755 index 00000000..e0aa9e07 --- /dev/null +++ b/scripts/farm/farm-sync.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# farm-sync.sh — 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 diff --git a/scripts/farm/farm-worker-loop.sh b/scripts/farm/farm-worker-loop.sh new file mode 100755 index 00000000..e47661f2 --- /dev/null +++ b/scripts/farm/farm-worker-loop.sh @@ -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 diff --git a/scripts/farm/hammer-farm.service b/scripts/farm/hammer-farm.service new file mode 100644 index 00000000..4ec755ed --- /dev/null +++ b/scripts/farm/hammer-farm.service @@ -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 diff --git a/scripts/farm/vps-setup.sh b/scripts/farm/vps-setup.sh new file mode 100755 index 00000000..ca4d02ba --- /dev/null +++ b/scripts/farm/vps-setup.sh @@ -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"