La deuda legal bloqueante del SDD 19 §2.1. Medido hoy: **0 de 1141 recetas** declaraban licencia, no «5 de 771» como decía el informe anterior. Los dos números estaban mal: los «5» eran falsos positivos de `grep license` (el paquete `addlicense`, el paquete `cargo-bundle-licenses`, una línea `install .../share/licenses/` y un comentario), y las recetas son 1141. Contar con `grep -l <palabra>` sobre TOML cuenta comentarios y nombres, no campos; `scripts/licencias.sh` cuenta el campo de verdad (clave en la raíz, antes del primer `[table]`). LO QUE HACE LA DEUDA PAGABLE: `Recipe::hash_inputs` es una LISTA BLANCA — sólo entran source, compiler, target, link, patches, flags, phases y deps. `license` no entra, igual que `evidence` y `slots`. Por eso se puede poblar en las recetas YA SELLADAS sin mover un solo ArtifactHash. Verificado, no supuesto: en 40 recetas modificadas se comparó el hash con y sin la línea — 40 idénticos, 0 cambiados. Si el campo entrara al hash, declarar la licencia costaría reconstruir el corpus entero y no se haría nunca. Clavado con el test `licencia_round_trip_y_no_afecta_el_hash`. TRAMPA DE TOML: una clave suelta después de un `[table]` pertenece a esa tabla. Puesta al final del fichero, `license` acaba dentro de `[deps]` y se pierde EN SILENCIO, porque serde ignora los campos que no conoce — no hay error, simplemente no está. Va arriba, junto a `name` y `version`; el sembrador la inserta tras `version`. NO SE ADIVINA. Declarar mal una licencia es peor que dejarla vacía: convierte un hueco visible en una afirmación falsa. Sólo se puebla desde una tabla curada entrada por entrada (`docs/licencias-conocidas.tsv`); lo que no tiene evidencia queda vacío y se CUENTA. Concretamente se descartó el atajo «k* = KDE ⇒ LGPL»: en este catálogo `kail`, `kind`, `ko`, `kopia`, `krew`, `kustomize`, `kyverno`, `katana`, `kibi`, `kmon` y toda la familia `kube*` son herramientas Go sin relación con KDE. El nombre no es evidencia. Quedan 913, casi todas CLIs Go/Rust importados en masa — y ésas sí son automatizables con evidencia real: Cargo.toml trae el campo `license` y los módulos Go traen su LICENSE en el árbol. El cierre estructural es capturarlo en la fase de fetch, que ya descarga y extrae cada tarball, y inyectar el texto en `hammer pack` (aguas abajo del ArtifactHash) en vez de en la fase install (que sí re-hashearía). De paso, respaldo-storagebox.sh reordenado por valor irreemplazable y con zstd: medido en la oficina, el uplink da 8 Mbps iguales por cable y por wifi ⇒ 128 G no caben en una sentada, así que sube primero el cerebro (estado + repo) y `--partial-dir` hace que cortar a mitad de un artefacto no tire lo ya subido. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
7.1 KiB
Bash
Executable File
102 lines
7.1 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"
|
|
|
|
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)"
|
|
$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)"
|
|
$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"
|
|
$RS $SECO -e "$SSH_CMD" --files-from="$HUER" "$RAIZ/store/" "$SB_USER@$SB_HOST:hammer/store/" || true
|
|
fi
|
|
echo " · resto del 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
|