tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1543Shell 203Rust 109Markdown 993728 Definitions

⎇ 5e42d201b16baa3d3696500cec114485a1dd4186 scripts/respaldo-storagebox.sh Shell · 131 lines · 8.7 KB History raw

respaldo: reintentar los cortes de red — el primero murió a los 2,3 G 5e42d201b · sergio ·

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