Files
hammer/scripts/respaldo-storagebox.sh
T
sergioandClaude Opus 5 5e42d201b1 respaldo: reintentar los cortes de red — el primero murió a los 2,3 G
La primera corrida real subió el cerebro (estado + repo, lo irreemplazable) y 2,3 G de
store, y entonces murió: `Connection reset by peer` / `Broken pipe`, rsync 255. Con ~19 h
de subida sobre un enlace de oficina, que la conexión se corte NO es un accidente: es lo
normal. Y un respaldo que hay que relanzar a mano cada vez que se cae no se completa nunca,
porque nadie está mirando.

Como rsync ya es incremental y `--partial-dir` conserva los trozos a medio subir,
reintentar es barato y seguro: cada intento retoma donde quedó, no reempieza. El bucle
insiste sólo ante fallos de RED (10/12/23/30/35/255) con espera creciente hasta 60 s; un
error de verdad —permisos, destino lleno, ruta inexistente— sale a la primera en vez de
repetirse 40 veces contra la misma pared.

DE PASO, UN ERROR MÍO QUE VALE REGISTRAR: comprobé el respaldo con `pgrep -f
respaldo-storagebox` y dijo que corría, cuando llevaba rato muerto — el pgrep se estaba
matcheando A SÍ MISMO, porque la cadena buscada está en la propia línea de comando del
shell que la ejecuta. Ya me había pasado hoy con el worker. La comprobación buena es mirar
lo que el proceso PRODUCE (bytes en el destino, última línea del log), no si hay un proceso
vivo. Es la misma lección que el guardián de store-gc: un `rm` que devuelve 0 no prueba que
el fichero se fue, y un proceso vivo no prueba que esté avanzando.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:30:58 -04:00

132 lines
8.7 KiB
Bash
Executable File

#!/bin/sh
# Respaldo de hammer al Storage Box de Hetzner (BX11, 1 TiB, hel1).
#
# ── POR QUÉ UN STORAGE BOX Y NO EL VOLUMEN ──────────────────────────────────────────────────────
# El usuario pidió respaldar «en el volumen, aunque sea montándolo aquí». **No se puede**: un Hetzner
# Cloud Volume es un dispositivo de bloque por RED que sólo se adjunta a servidores de Hetzner Cloud
# del mismo datacenter — no se monta en un laptop, y encima sólo puede estar en UN servidor a la vez.
# `harkaq-cosecha` (100 G) es el disco del worker efímero y `vvv` (260 G, en gioser) está al 78%.
# El Storage Box SÍ es el producto para esto: se monta desde acá (SSHFS/CIFS) y habla rsync/borg/
# restic por SSH. BX11 = 1 TiB por €3,20/mes, sin coste de alta. Ver [[capacidad-disco-proyeccion]].
#
# ── ES INTERRUMPIBLE Y CONTINUABLE. ESE ES EL DISEÑO, NO UN EFECTO SECUNDARIO ───────────────────
# Medido en la oficina el 2026-08-07: el enlace da **8 Mbps de subida**, idénticos por cable (eth0)
# y por wifi (wlan0) ⇒ el cuello NO está en el laptop, está en el uplink. Con 128 G de store eso son
# ~35 h sin comprimir. Por lo tanto el respaldo NO se completa en una sentada, y todo el script está
# construido alrededor de esa realidad:
# · rsync incremental: lo ya subido no se vuelve a subir (el store es CAS, los ficheros nunca
# cambian, así que la comparación por tamaño+fecha es exacta y baratísima).
# · `--partial-dir`: un fichero cortado a la mitad conserva el trozo y la próxima corrida lo
# TERMINA en vez de empezarlo de cero. Sin esto, cortar a mitad de un artefacto de 500 MB
# tiraría los 400 MB ya subidos.
# · Se puede matar con Ctrl-C o apagar el laptop en cualquier momento. Se relanza y sigue.
#
# ── EL ORDEN ES POR VALOR IRREEMPLAZABLE, NO POR TAMAÑO ─────────────────────────────────────────
# Como no cabe todo en una ventana, lo que sube primero importa. De más a menos crítico:
# 1. ESTADO Y REPO (~cientos de MB, minutos). Recetas, scripts, SDDs, el grafo. Es el CEREBRO: con
# esto solo, el store entero se reconstruye — caro en CPU, pero se reconstruye. Sin esto no hay
# proyecto. Va primero aunque ya esté espejado a GitHub, porque un respaldo que depende de otro
# respaldo no es un respaldo.
# 2. HUÉRFANOS del store (~4,5 G). Artefactos que son el ÚNICO ejemplar de su receta: su hash
# vigente no está sellado, así que perderlos es perder trabajo real, no reconstruible en dos
# minutos. Es el material genuinamente irreemplazable y es sorprendentemente barato.
# 3. EL RESTO DEL STORE (~128 G). Meses de CPU, pero todo reconstruible desde (1).
#
# ── POR QUÉ rsync PLANO Y NO borg/restic ────────────────────────────────────────────────────────
# El store es CAS: los ficheros son INMUTABLES y se nombran por hash. Nunca cambian, sólo aparecen o
# desaparecen. Para eso rsync incremental es exactamente lo correcto y no hay que mantener repos ni
# claves de cifrado. Sí se usa COMPRESIÓN zstd, que aquí no es un lujo: medido, 53 MB tardan 53 s sin
# comprimir y 27 s con `--compress-choice=zstd` — **el doble de rendimiento efectivo**, porque el 79%
# del contenido del store son secciones `.debug_*` y comprimen como texto. Con un enlace de 8 Mbps la
# CPU sobra y el ancho de banda es el recurso escaso: comprimir siempre gana.
#
# ── EL STORE NO SE BORRA EN EL DESTINO, A PROPÓSITO ─────────────────────────────────────────────
# `--delete` NO va sobre el store. Un respaldo que replica los borrados no protege del borrado por
# error, y el 2026-08-07 `store-gc.sh` demostró que puede equivocarse EN SILENCIO (reportó 364
# artefactos borrados sin haber borrado ninguno). El respaldo acumula; podarlo es una decisión
# deliberada y aparte. El repo sí va con `--delete`, que para eso está git.
#
# Uso: scripts/respaldo-storagebox.sh [--seco]
# Relanzalo cuantas veces quieras: continúa donde quedó.
set -eu
SB_USER="${SB_USER:-u647150}"
SB_HOST="${SB_HOST:-u647150.your-storagebox.de}"
SB_PORT="${SB_PORT:-23}" # 23 = SSH completo (rsync). El 22 sólo da SFTP/SCP restringido.
KEY="${KEY:-$HOME/.ssh/github5}"
RAIZ="${RAIZ:-/home/sergio/hammer}"
SECO=""
[ "${1:-}" = "--seco" ] && SECO="--dry-run"
# `-4` a propósito: el box tiene AAAA y el laptop tiene IPv6 sólo en wlan0, así que sin esto la ruta
# por cable nunca se usaría aunque el cable estuviera enchufado.
SSH_CMD="ssh -4 -p $SB_PORT -i $KEY -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30"
# Opciones comunes. `--partial-dir` es lo que hace el respaldo continuable a mitad de fichero.
RS="rsync -a --partial-dir=.rsync-partial --info=progress2 -z --compress-choice=zstd --compress-level=3"
# ── REINTENTOS: EL ENLACE SE CAE, Y ESO NO ES EXCEPCIONAL ───────────────────────────────────────
# Primera corrida real (2026-08-07): tras subir el cerebro y 2,3 G de store, murió con
# `Connection reset by peer` / `Broken pipe` y rsync salió con 255. Con una subida de ~19 h sobre un
# enlace de oficina, que la conexión se corte no es un accidente: es lo NORMAL. Un respaldo que hay
# que relanzar a mano cada vez que se cae no se completa nunca, porque nadie está mirando.
#
# Como rsync ya es incremental y `--partial-dir` conserva los trozos, reintentar es barato y seguro:
# cada intento retoma donde quedó. El bucle sólo insiste ante fallos de RED (códigos 10/12/23/30/35/
# 255); un error de verdad —permisos, disco lleno en destino, ruta inexistente— sale a la primera en
# vez de repetirse 40 veces contra la misma pared.
reintentar() {
etiqueta="$1"; shift
intento=1
while :; do
"$@" && return 0
rc=$?
case "$rc" in
10|12|23|30|35|255) ;; # red / pipe roto / timeout: insistir
*) echo "!! $etiqueta: error $rc que NO es de red — no reintento"; return "$rc" ;;
esac
if [ "$intento" -ge 40 ]; then
echo "!! $etiqueta: 40 intentos y sigue cayéndose. Dejo lo subido y paro."; return "$rc"
fi
espera=$(( intento < 6 ? intento * 10 : 60 ))
echo ".. $etiqueta: corte de red (rsync $rc). Intento $intento; reanudo en ${espera}s."
intento=$((intento + 1))
sleep "$espera"
done
}
if ! $SSH_CMD "$SB_USER@$SB_HOST" 'df -h .' >/dev/null 2>&1; then
echo "!! No hay SSH al Storage Box ($SB_HOST:$SB_PORT)."
echo "!! Si acabás de crearlo, el DNS tarda unos minutos en propagar. Reintentá."
exit 1
fi
echo "==> destino: $SB_USER@$SB_HOST:$SB_PORT ${SECO:+(SECO)}"
$SSH_CMD "$SB_USER@$SB_HOST" 'mkdir hammer hammer/store hammer/repo hammer/estado' >/dev/null 2>&1 || true
# ── 1. EL CEREBRO: estado + repo. Minutos, y es lo que no se puede perder. ──────────────────────
echo "==> [1/3] estado (el grafo: qué había construido y con qué hash)"
reintentar estado $RS $SECO -e "$SSH_CMD" "$RAIZ/docs/state/" "$SB_USER@$SB_HOST:hammer/estado/" || true
echo "==> [2/3] repo (recetas, scripts, SDDs — el cerebro; con esto solo se reconstruye el store)"
reintentar repo $RS --delete $SECO -e "$SSH_CMD" \
--exclude /store --exclude /store-rust --exclude /store-kern \
--exclude /work --exclude /target --exclude /.dev-fs --exclude /dist --exclude /.scratch \
"$RAIZ/" "$SB_USER@$SB_HOST:hammer/repo/"
# ── 2 y 3. EL STORE, huérfanos primero. ────────────────────────────────────────────────────────
# La lista de huérfanos la produce `store-gc.sh` como subproducto; si no hay una reciente, se sube
# el store entero en un solo paso y santas pascuas (rsync es incremental: no se pierde nada).
echo "==> [3/3] store (128 G a 8 Mbps ⇒ NO cabe en una sentada; esto se corta y se continúa)"
HUER="$(ls -t "$RAIZ"/work/store-gc-huerfanos-*.txt 2>/dev/null | head -1)"
if [ -n "$HUER" ] && [ -s "$HUER" ]; then
echo " · huérfanos primero ($(wc -l < "$HUER") artefactos, únicos ejemplares) — desde $HUER"
reintentar huerfanos $RS $SECO -e "$SSH_CMD" --files-from="$HUER" "$RAIZ/store/" "$SB_USER@$SB_HOST:hammer/store/" || true
fi
echo " · resto del store"
reintentar store $RS $SECO -e "$SSH_CMD" "$RAIZ/store/" "$SB_USER@$SB_HOST:hammer/store/"
echo "==> ocupación en el Storage Box:"
$SSH_CMD "$SB_USER@$SB_HOST" 'df -h .' 2>/dev/null | tail -2