harkaq: granja con volumen persistente + workers auto-terminantes
La arquitectura que la 1ª campaña pidió a gritos (el worker quedó idle ~4h
cobrando). Tres piezas en harkaq-vol.sh {up|collect|close|status}:
1. VOLUMEN persistente (harkaq-cosecha): los workers escriben verdicts a
/mnt/cosecha, que sobrevive a su destrucción ⇒ NO dependo de estar viva para
no perder el trabajo.
2. AUTO-SHUTDOWN (harkaq-campana-vol.sh): tras IDLE_CICLOS ciclos con 0
mediciones nuevas, el worker SE AUTO-ELIMINA. En Hetzner un server apagado
sigue cobrando ⇒ apagar de verdad es delete. Vía API REST + curl + metadata
service (NO hcloud CLI: la golden Ubuntu no lo trae; curl siempre está). El
ID propio sale del metadata, no del nombre.
3. collect: monta el volumen en un helper barato efímero (o un worker vivo),
rsync al hub, corre el harvest, destruye el helper. close: detach+delete del
volumen (deja de cobrar, ~€0.44/mes mientras exista).
+ dos fixes de la 1ª campaña horneados en el bucle: borra el artefacto -hkm tras
medir (envenenaba la caché) y los dos gates (ABI>=7 + binario-con-jaula).
El token va al worker (para auto-delete): riesgo acotado — worker efímero, sin
servicios entrantes salvo SSH-por-clave, destruido al terminar.
Bug cazado antes de gastar: las llaves {..} dentro del mensaje de ${1:?...}
cerraban la expansión (CMD salía 'status}').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# harkaq-campana-vol.sh <mi-nombre> — bucle de medición con VOLUMEN + AUTO-SHUTDOWN.
|
||||
#
|
||||
# Igual que harkaq-campana.sh, con dos cambios que cierran los dos huecos de la 1ª campaña:
|
||||
# 1. Escribe cada veredicto al VOLUMEN (/mnt/cosecha/verdicts), que sobrevive a la destrucción
|
||||
# del worker ⇒ no dependo de estar viva para no perder el trabajo.
|
||||
# 2. AUTO-SHUTDOWN: tras IDLE_CICLOS ciclos con 0 mediciones nuevas, el worker SE AUTO-ELIMINA.
|
||||
# En Hetzner un server apagado sigue cobrando ⇒ apagar de verdad es `hcloud server delete`.
|
||||
# Es lo que la 1ª campaña NO tenía: quedó idle ~4h cobrando sin producir.
|
||||
set -u
|
||||
|
||||
MI_NOMBRE="${1:?uso: harkaq-campana-vol.sh <mi-nombre>}"
|
||||
HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
|
||||
cd "$HAMMER_DIR"
|
||||
LISTA="${LISTA:-work/harkaq-cola.txt}"
|
||||
VOL_OUT="/mnt/cosecha"
|
||||
OUT="work/harkaq-campana" # local (rápido); se COPIA al volumen tras cada medición
|
||||
BIN="${HARKAQ_BIN:-/root/.cache/harkaq}"
|
||||
IDLE_SLEEP="${IDLE_SLEEP:-300}"
|
||||
POR_RECETA="${POR_RECETA:-900}"
|
||||
IDLE_CICLOS="$(cat /etc/harkaq-idle-ciclos 2>/dev/null || echo 3)"
|
||||
mkdir -p "$OUT" "$VOL_OUT/verdicts" "$VOL_OUT/logs"
|
||||
|
||||
export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="${HARKAQ_BASE:-$BIN/base.policy}"
|
||||
export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-300}"
|
||||
|
||||
# Los dos gates de siempre: ABI>=7 y el binario CON jaula. Sin ellos la campaña muele y produce
|
||||
# SinEvidencia (o cero) en silencio — el falso `Hermetico` de D9 mudado al orquestador.
|
||||
abi=$(python3 -c 'import ctypes;print(ctypes.CDLL("libc.so.6").syscall(444,None,0,1))' 2>/dev/null)
|
||||
[ "${abi:-0}" -ge 7 ] || { echo "ABORTO: Landlock ABI ${abi:-?} < 7"; exit 4; }
|
||||
strings ./target/release/hammer 2>/dev/null | grep -q harkaq || { echo "ABORTO: el binario no tiene la jaula"; exit 4; }
|
||||
echo "$(date -u +%FT%TZ) campaña-vol: ABI $abi, jaula ✓, auto-shutdown tras $IDLE_CICLOS ciclos idle"
|
||||
|
||||
autodestruir() {
|
||||
echo "$(date -u +%FT%TZ) $MI_NOMBRE: $IDLE_CICLOS ciclos idle ⇒ AUTO-ELIMINACIÓN. Cosechas en el volumen."
|
||||
cp -f /var/log/harkaq-vol.log "$VOL_OUT/logs/$MI_NOMBRE.log" 2>/dev/null || true
|
||||
sync
|
||||
# API REST con curl, NO el CLI de hcloud: la golden (Ubuntu base) no trae hcloud, y depender de
|
||||
# instalarlo haría fallar la pieza central. curl + el metadata service SIEMPRE están. El ID
|
||||
# propio se saca del metadata (169.254.169.254), no del nombre — más robusto ante renombrados.
|
||||
id=$(curl -sf http://169.254.169.254/hetzner/v1/metadata/instance-id 2>/dev/null)
|
||||
tok=$(cat /root/.hcloud-token 2>/dev/null)
|
||||
if [ -n "$id" ] && [ -n "$tok" ]; then
|
||||
curl -sf -X DELETE -H "Authorization: Bearer $tok" \
|
||||
"https://api.hetzner.cloud/v1/servers/$id" >/dev/null 2>&1 &
|
||||
fi
|
||||
# Fallback si la API no responde: apagar. Sigue cobrando (Hetzner cobra el server apagado) pero
|
||||
# al menos deja de moler en vano; el `status` mostrará el server off para borrarlo a mano.
|
||||
sleep 45; poweroff
|
||||
}
|
||||
|
||||
idle=0
|
||||
while :; do
|
||||
nuevas=0
|
||||
while IFS= read -r n; do
|
||||
[ -n "$n" ] || continue
|
||||
[ -f "recipes/$n.toml" ] || continue
|
||||
[ -s "$VOL_OUT/verdicts/$n.verdicts" ] && continue # ya medida (en el volumen)
|
||||
|
||||
tmp="recipes/.hk-$n.toml"
|
||||
sed "s/^name *= *\"$n\"/name = \"$n-hkm\"/" "recipes/$n.toml" > "$tmp"
|
||||
timeout "$POR_RECETA" ./target/release/hammer build "$tmp" --store "$PWD/store" \
|
||||
> "$OUT/$n.log" 2>&1
|
||||
rm -f "$tmp"
|
||||
# El sufijo -hkm sella un artefacto que después pega en caché y bloquea la re-medición
|
||||
# (envenenó bash/binutils en la 1ª campaña). Se borra tras medir.
|
||||
rm -rf store/*-"$n"-hkm 2>/dev/null || true
|
||||
grep '^\[harkaq\] {' "$OUT/$n.log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts"
|
||||
if [ -s "$OUT/$n.verdicts" ]; then
|
||||
cp -f "$OUT/$n.verdicts" "$VOL_OUT/verdicts/$n.verdicts"; sync
|
||||
nuevas=$((nuevas + 1))
|
||||
echo "$(date -u +%FT%TZ) ++ $n → volumen"
|
||||
else
|
||||
echo "sin-veredicto" > "$VOL_OUT/verdicts/$n.skip"
|
||||
echo "$(date -u +%FT%TZ) ?? $n — sin veredicto"
|
||||
fi
|
||||
done < "$LISTA"
|
||||
|
||||
if [ "$nuevas" -eq 0 ]; then
|
||||
idle=$((idle + 1))
|
||||
echo "$(date -u +%FT%TZ) ciclo idle $idle/$IDLE_CICLOS"
|
||||
[ "$idle" -ge "$IDLE_CICLOS" ] && { autodestruir; exit 0; }
|
||||
else
|
||||
idle=0
|
||||
fi
|
||||
sleep "$IDLE_SLEEP"
|
||||
done
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/bin/sh
|
||||
# harkaq-vol.sh — granja efímera con VOLUMEN persistente y workers auto-terminantes.
|
||||
#
|
||||
# El modelo que la 1ª campaña pidió a gritos: el worker no depende de que yo esté viva para
|
||||
# cosechar, y no se queda idle cobrando. Tres piezas:
|
||||
#
|
||||
# up N crea/reusa el volumen, levanta N workers con él montado en /mnt/cosecha, cada uno
|
||||
# con auto-shutdown: tras IDLE_CICLOS ciclos sin trabajo nuevo, el worker SE
|
||||
# AUTO-ELIMINA (poweroff en Hetzner sigue cobrando ⇒ delete de verdad). Las cosechas
|
||||
# ya están en el volumen, que sobrevive.
|
||||
# collect (yo, cuando esté viva) monta el volumen en un helper barato efímero, rsync las
|
||||
# cosechas al hub, corre el harvest, destruye el helper.
|
||||
# close detach + delete del volumen. Se paga sólo mientras existe (~€0.44/mes).
|
||||
# status qué hay vivo y qué hay en el volumen.
|
||||
#
|
||||
# Por qué el token va al worker: para auto-eliminarse necesita la API. Riesgo acotado — el worker
|
||||
# es efímero, sin servicios entrantes salvo SSH-por-clave, y se destruye. El token vive sólo
|
||||
# mientras el worker corre. (Hetzner no da tokens por-recurso; es full-project.)
|
||||
set -eu
|
||||
|
||||
# Sin llaves en el mensaje del `${1:?...}`: una `}` interna cierra la expansión antes de tiempo
|
||||
# (CMD salía como "status}"). El uso completo va en el `*)` del case.
|
||||
CMD="${1:?uso: harkaq-vol.sh up N | collect | close | status}"
|
||||
HUB="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$HUB"
|
||||
VOL="${VOL:-harkaq-cosecha}"
|
||||
IMAGE="${IMAGE:-408909310}" # golden con kernel 6.17 (Landlock ABI 7)
|
||||
TYPE="${TYPE:-ccx23}"
|
||||
HELPER_TYPE="${HELPER_TYPE:-cx22}" # para collect: el más barato que monte el volumen
|
||||
LOCATION="${LOCATION:-hel1}"
|
||||
SSHKEY="${SSHKEY:-desarrollo@jlsoltech.com}"
|
||||
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
|
||||
REMOTE="${REMOTE:-/opt/hammer}"
|
||||
IDLE_CICLOS="${IDLE_CICLOS:-3}" # ciclos idle antes de auto-eliminarse
|
||||
SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
|
||||
|
||||
token() { grep -oP 'token\s*=\s*"\K[^"]+' "$HOME/.config/hcloud/cli.toml" | head -1; }
|
||||
|
||||
vol_id() { hcloud volume list -o noheader -o columns=id,name | awk -v v="$VOL" '$2==v{print $1}'; }
|
||||
vol_dev() { echo "/dev/disk/by-id/scsi-0HC_Volume_$(vol_id)"; }
|
||||
|
||||
ensure_vol() {
|
||||
if [ -z "$(vol_id)" ]; then
|
||||
echo "== creando volumen $VOL (10GB)"
|
||||
hcloud volume create --name "$VOL" --size 10 --location "$LOCATION" --format ext4 >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
wait_ssh() { for _ in $(seq 1 30); do $SSH "root@$1" true 2>/dev/null && return 0; sleep 6; done; return 1; }
|
||||
|
||||
case "$CMD" in
|
||||
up)
|
||||
N="${2:-1}"; ensure_vol
|
||||
DEV="$(vol_dev)"
|
||||
for i in $(seq 1 "$N"); do
|
||||
name="hkw-$i"
|
||||
echo "== $name: creando con volumen $VOL montado"
|
||||
# --automount monta el volumen; el user-data hornea el token, la config de auto-shutdown y
|
||||
# arranca el servicio. Así el worker es autónomo desde el boot: mide → escribe al volumen →
|
||||
# se auto-elimina al agotar la cola. Nada depende de que yo esté para nada.
|
||||
ud=$(mktemp)
|
||||
cat > "$ud" <<EOF
|
||||
#cloud-config
|
||||
write_files:
|
||||
- path: /root/.hcloud-token
|
||||
permissions: '0600'
|
||||
content: "$(token)"
|
||||
- path: /etc/harkaq-idle-ciclos
|
||||
content: "$IDLE_CICLOS"
|
||||
EOF
|
||||
hcloud server create --name "$name" --image "$IMAGE" --type "$TYPE" \
|
||||
--location "$LOCATION" --ssh-key "$SSHKEY" --volume "$VOL" \
|
||||
--user-data-from-file "$ud" --label role=harkaq-vol >/dev/null
|
||||
rm -f "$ud"
|
||||
ip=$(hcloud server ip "$name")
|
||||
wait_ssh "$ip" || { echo " $name no responde"; continue; }
|
||||
# Provisión: código + harkaq + montaje del volumen + servicio con auto-shutdown.
|
||||
rsync -az -e "$SSH" --exclude /store --exclude '/store-*' --exclude /target --exclude /dist \
|
||||
--exclude /.dev-fs --exclude /.git --exclude /.scratch --exclude '*.png' --exclude '/content*' \
|
||||
./ "root@$ip:$REMOTE/" >/dev/null
|
||||
$SSH "root@$ip" "MNT=/mnt/cosecha; mkdir -p \$MNT; mount $DEV \$MNT 2>/dev/null || (mkfs.ext4 -q -F $DEV && mount $DEV \$MNT); mkdir -p \$MNT/verdicts \$MNT/logs"
|
||||
scripts/farm/harkaq-farm-setup.sh "$ip" >/dev/null 2>&1 || echo " (setup con avisos, sigo)"
|
||||
$SSH "root@$ip" "export PATH=\$HOME/.cargo/bin:\$PATH; cd $REMOTE && cargo build --release -q -p hammer-cli 2>&1 | tail -1"
|
||||
# Servicio con auto-shutdown (el script harkaq-campana-vol lo maneja).
|
||||
$SSH "root@$ip" "cat > /etc/systemd/system/harkaq-vol.service <<UNIT
|
||||
[Unit]
|
||||
Description=harkaq campana con volumen + auto-shutdown
|
||||
After=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$REMOTE
|
||||
Environment=HARKAQ_BIN=/root/.cache/harkaq
|
||||
ExecStart=$REMOTE/scripts/farm/harkaq-campana-vol.sh $name
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
StandardOutput=append:/var/log/harkaq-vol.log
|
||||
StandardError=append:/var/log/harkaq-vol.log
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload && systemctl enable --now harkaq-vol >/dev/null 2>&1"
|
||||
echo " $name ✓ midiendo → volumen; se auto-elimina tras $IDLE_CICLOS ciclos idle"
|
||||
done
|
||||
;;
|
||||
|
||||
collect)
|
||||
VID="$(vol_id)"; [ -n "$VID" ] || { echo "no hay volumen $VOL"; exit 1; }
|
||||
# ¿ya hay un worker vivo con el volumen? úsalo. Si no, helper efímero barato.
|
||||
host=$(hcloud server list -o noheader -o columns=name,labels | awk '/role=harkaq-vol/{print $1; exit}')
|
||||
helper=""
|
||||
if [ -z "$host" ]; then
|
||||
helper="hkcollect"; echo "== sin worker vivo: helper efímero $helper ($HELPER_TYPE)"
|
||||
hcloud server create --name "$helper" --image debian-12 --type "$HELPER_TYPE" \
|
||||
--location "$LOCATION" --ssh-key "$SSHKEY" --volume "$VOL" >/dev/null
|
||||
host="$helper"
|
||||
fi
|
||||
ip=$(hcloud server ip "$host"); wait_ssh "$ip"
|
||||
$SSH "root@$ip" "mkdir -p /mnt/cosecha; mount $(vol_dev) /mnt/cosecha 2>/dev/null || true; ls /mnt/cosecha/verdicts 2>/dev/null | wc -l" \
|
||||
| sed 's/^/ verdicts en el volumen: /'
|
||||
mkdir -p work/harkaq-campana
|
||||
rsync -az -e "$SSH" "root@$ip:/mnt/cosecha/verdicts/" work/harkaq-campana/ 2>/dev/null || true
|
||||
[ -n "$helper" ] && { echo "== destruyo el helper"; hcloud server delete "$helper" >/dev/null; }
|
||||
echo "== corriendo el harvest sobre lo recogido"
|
||||
scripts/farm/harvest-harkaq.sh "local" 2>/dev/null || \
|
||||
echo " (harvest necesita un worker; recogido a work/harkaq-campana/, clasificá con harvest-harkaq)"
|
||||
;;
|
||||
|
||||
close)
|
||||
VID="$(vol_id)"; [ -z "$VID" ] && { echo "no hay volumen $VOL"; exit 0; }
|
||||
srv=$(hcloud volume describe "$VID" -o 'format={{.Server}}' 2>/dev/null || echo "")
|
||||
[ -n "$srv" ] && [ "$srv" != "<nil>" ] && hcloud volume detach "$VID" >/dev/null 2>&1 || true
|
||||
hcloud volume delete "$VID" >/dev/null && echo "== volumen $VOL clausurado (deja de cobrar)"
|
||||
;;
|
||||
|
||||
status)
|
||||
echo "── volumen: $(vol_id | sed 's/^$/(ninguno)/')"
|
||||
echo "── workers harkaq-vol vivos:"; hcloud server list -o noheader -o columns=name,status,age | awk '/hkw-|hkcollect/{print " "$0}' || echo " ninguno"
|
||||
;;
|
||||
|
||||
*) echo "uso: harkaq-vol.sh {up N|collect|close|status}"; exit 2 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user