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>
This commit is contained in:
2026-08-09 21:58:01 -04:00
co-authored by Claude Opus 5
parent 400f7171e0
commit ddc8561e07
2 changed files with 47 additions and 17 deletions
+41 -16
View File
@@ -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
+6 -1
View File
@@ -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».