buzón verificado en un worker real — y las dos veces que la prueba mintió
Cadena completa medida hoy: el worker depositó 63 artefactos (1,34 GB) en 8 s a 153 MB/s, y el hub los promovió con mv. Respaldo 1542 → 1605, buzón en 0, guardián cuadrando. Contra los 440 kB/s del laptop, 348×. Dos fallos de la prueba, que importan más que el resultado: 1. `ssh` cortaba con «Host key verification failed» porque la granja REUSA IPs y la host key cambia con razón. Como el stderr iba a /dev/null, el error salía como «no obtuve la pública del worker»: culpaba al worker cuando el problema estaba en el known_hosts del laptop. Ahora va por ssh_worker(), que purga la entrada vieja — correcto sólo acá, porque la identidad del worker es su label hcloud, no su llave. 2. verificar-buzon daba «aislado ✓» sin haber probado que escribe. El aviso de host key se comía el head -1, y el testigo se llamaba .btest, que `ls` no muestra. Un testigo invisible no prueba nada. Ahora exige las dos mitades y las dos están verdes: escribe en su buzón, y ../hammer/store no existe para él. Y el progreso de rsync sólo con TTY: sin terminal reescribe con \r y deja miles de copias de la misma línea. 8 segundos bastaron para un log ilegible; en el latido sería cada media hora. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
2 archivos · +47 −17
diff --git a/scripts/farm/respaldo-subcuenta.sh b/scripts/farm/respaldo-subcuenta.sh
index 9eb6f178..8a7be7a2 100755
--- a/scripts/farm/respaldo-subcuenta.sh
+++ b/scripts/farm/respaldo-subcuenta.sh
@@ -71,6 +71,20 @@ BUZ_USER="${BUZ_USER:-u647150-sub2}"
KEY="${KEY:-$HOME/.ssh/github5}"
CRED="$HOME/.config/hammer/storagebox-worker.env"
+# ── LA GRANJA REUSA IPs, ASÍ QUE LA HOST KEY CAMBIA CON RAZÓN ──────────────────────────────────
+# Los workers son EFÍMEROS y Hetzner recicla las direcciones: una IP que ayer fue `hworker-1` hoy es
+# otra máquina distinta con otra llave. Sin esto, `ssh` corta con «Host key verification failed» —
+# y como estas funciones mandaban stderr a /dev/null, el fallo salía como «no obtuve la pública del
+# worker», que apunta al worker cuando el problema estaba en el known_hosts del laptop. Un mensaje
+# que culpa al sitio equivocado cuesta más que no tener mensaje.
+# Purgar la entrada vieja es correcto ACÁ y sólo acá: la identidad del worker no es su llave, es el
+# label `role=hammer-worker` del proyecto hcloud; el secreto no viaja por este canal.
+ssh_worker() { # $1 = ip, resto = orden remota
+ ip="$1"; shift
+ ssh-keygen -R "$ip" >/dev/null 2>&1
+ ssh -i "$KEY" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20 "root@$ip" "$@"
+}
+
case "${1:-}" in
crear)
# La contraseña la exige la API aunque después se use llave. Debe traer mayúscula, minúscula,
@@ -85,9 +99,9 @@ crear)
;;
autorizar)
IP="${2:?falta la IP del worker}"
- PUB=$(ssh -i "$KEY" "root@$IP" 'mkdir -p /root/.ssh
+ PUB=$(ssh_worker "$IP" 'mkdir -p /root/.ssh
[ -f /root/.ssh/sb_sub ] || ssh-keygen -q -t ed25519 -N "" -C "hworker→respaldo(ro)" -f /root/.ssh/sb_sub
- cat /root/.ssh/sb_sub.pub' 2>/dev/null | tail -1)
+ cat /root/.ssh/sb_sub.pub' | tail -1)
[ -z "$PUB" ] && { echo "!! no obtuve la pública del worker"; exit 1; }
T=$(mktemp); printf '%s\n' "$PUB" > "$T"
ssh -4 -p 23 -i "$KEY" "$MAIN" 'mkdir -p hammer/.ssh' >/dev/null 2>&1
@@ -97,7 +111,7 @@ autorizar)
;;
verificar)
IP="${2:?falta la IP del worker}"
- ssh -i "$KEY" "root@$IP" "SB=\"ssh -4 -p 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no $SUB_USER@$SUB_HOST\"
+ ssh_worker "$IP" "SB=\"ssh -4 -p 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no $SUB_USER@$SUB_HOST\"
lee=\$(\$SB 'ls' 2>/dev/null | tr '\n' ' ')
borra=\$(\$SB 'rm -rf estado' 2>&1 | head -1)
echo hola > /tmp/.wtest; esc=\$(scp -4 -P 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no /tmp/.wtest $SUB_USER@$SUB_HOST:.wtest 2>&1 | head -1)
@@ -113,9 +127,9 @@ autorizar-buzon)
IP="${2:?falta la IP del worker}"
# La MISMA llave del worker sirve para las dos subcuentas: son dos autorizaciones distintas del
# box, no dos identidades del worker. Se genera allá y nunca viaja una privada desde el laptop.
- PUB=$(ssh -i "$KEY" "root@$IP" 'mkdir -p /root/.ssh
+ PUB=$(ssh_worker "$IP" 'mkdir -p /root/.ssh
[ -f /root/.ssh/sb_sub ] || ssh-keygen -q -t ed25519 -N "" -C "hworker→respaldo" -f /root/.ssh/sb_sub
- cat /root/.ssh/sb_sub.pub' 2>/dev/null | tail -1)
+ cat /root/.ssh/sb_sub.pub' | tail -1)
[ -z "$PUB" ] && { echo "!! no obtuve la pública del worker"; exit 1; }
T=$(mktemp); printf '%s\n' "$PUB" > "$T"
ssh -4 -p 23 -i "$KEY" "$MAIN" 'mkdir -p incoming/.ssh' >/dev/null 2>&1
@@ -129,18 +143,29 @@ verificar-buzon)
# La verificación tiene DOS mitades y las dos son necesarias: que SÍ escriba (si no, el respaldo
# no se alimenta y nos enteramos el día que haga falta) y que NO alcance el respaldo consolidado
# (si lo alcanza, el aislamiento es decorativo). Una sola mitad no prueba nada útil.
- ssh -i "$KEY" "root@$IP" "SB=\"ssh -4 -p 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no $BUZ_USER@$BUZ_HOST\"
- echo hola > /tmp/.btest
- esc=\$(scp -4 -P 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no /tmp/.btest $BUZ_USER@$BUZ_HOST:store/.btest 2>&1 | head -1)
- ve=\$(\$SB 'ls store' 2>&1 | head -3 | tr '\n' ' ')
- fuera=\$(\$SB 'ls ../hammer/store' 2>&1 | head -1)
- \$SB 'rm -f store/.btest' >/dev/null 2>&1
- echo \"escribir en buzón : \${esc:-(sin error = OK)}\"
- echo \"ve su buzón : \$ve\"
+ # ⚠ DOS DETALLES QUE HICIERON QUE ESTA PRUEBA MINTIERA LA PRIMERA VEZ (2026-08-09):
+ # 1. el «Warning: Permanently added …» del host key sale por stderr y se comía el `head -1`, así
+ # que el resultado de escribir era el aviso y no el resultado. Se filtra explícitamente.
+ # 2. el testigo se llamaba `.btest` y `ls` NO muestra los ficheros con punto ⇒ el listado salía
+ # vacío tanto si escribió como si no. Un testigo que no se puede ver no prueba nada.
+ ssh_worker "$IP" "SB=\"ssh -4 -p 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no $BUZ_USER@$BUZ_HOST\"
+ limpia() { grep -v 'Warning: Permanently added' | head -1; }
+ echo testigo > /tmp/testigo-buzon.txt
+ esc=\$(scp -4 -P 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no /tmp/testigo-buzon.txt $BUZ_USER@$BUZ_HOST:store/testigo-buzon.txt 2>&1 | limpia)
+ ve=\$(\$SB 'ls store' 2>/dev/null | grep -c testigo-buzon)
+ fuera=\$(\$SB 'ls ../hammer/store' 2>&1 | limpia)
+ borra=\$(\$SB 'rm -f store/testigo-buzon.txt' 2>&1 | limpia)
+ queda=\$(\$SB 'ls store' 2>/dev/null | grep -c testigo-buzon)
+ echo \"escribir en buzón : \${esc:-(sin error)}\"
+ echo \"testigo visible : \$ve (1 = escribió de verdad)\"
+ echo \"limpia lo suyo : \${borra:-(sin error)} → queda \$queda\"
echo \"alcanza respaldo : \$fuera\"
- case \"\$fuera\" in *[Nn]o\ such*|*denied*|*[Ff]ailure*|'')
- echo 'VEREDICTO: buzón aislado ✓' ;;
- *) echo 'VEREDICTO: ⚠ ALCANZA EL RESPALDO — no usar'; exit 1 ;; esac"
+ ok=1
+ [ \"\$ve\" = 1 ] || { echo 'VEREDICTO: ⚠ NO ESCRIBE — el respaldo no se alimentaría'; ok=0; }
+ case \"\$fuera\" in *[Nn]o\ such*|*denied*|*[Ff]ailure*) ;;
+ *) echo 'VEREDICTO: ⚠ ALCANZA EL RESPALDO — no usar'; ok=0 ;; esac
+ [ \"\$ok\" = 1 ] && echo 'VEREDICTO: escribe en su buzón ✓ · no alcanza el respaldo ✓'
+ [ \"\$ok\" = 1 ] || exit 1"
;;
*) sed -n '/^# Uso:/,/verificar-buzon/p' "$0"; exit 2 ;;
esac
diff --git a/scripts/farm/worker-depositar.sh b/scripts/farm/worker-depositar.sh
index d67fafcd..52da2688 100755
--- a/scripts/farm/worker-depositar.sh
+++ b/scripts/farm/worker-depositar.sh
@@ -41,7 +41,12 @@ echo "==> store $N_L · respaldo $N_R ⇒ a depositar $N_N"
[ "$N_N" -eq 0 ] && { echo " nada nuevo"; exit 0; }
# `--files-from` con los nombres a secas (son directorios del store) + `-r` para bajar en cada uno.
-rsync -aHr --files-from="$TMP/nuevos.txt" --partial-dir=.rsync-partial --info=progress2 \
+# `--info=progress2` SÓLO con terminal: sin TTY reescribe la misma línea con \r y, como nadie
+# interpreta el retorno de carro, el «progreso» se acumula en miles de copias de la misma línea. Un
+# depósito de 8 segundos dejó así un log ilegible; en el latido, cada media hora, sería peor.
+PROG=""; [ -t 1 ] && PROG="--info=progress2"
+# shellcheck disable=SC2086
+rsync -aHr --files-from="$TMP/nuevos.txt" --partial-dir=.rsync-partial $PROG \
-e "$SSH_SB" "$STORE/" "$BZ_USER@$BZ_HOST:store/" || { echo "⚠ el depósito falló"; exit 1; }
# Guardián: recontar el buzón. «rsync salió 0» no es «llegaron N».