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…

⎇ 476168bb077db780e49c83e3ca4f0d3d230bd58a scripts/farm/respaldo-promover.sh Shell · 104 lines · 6.3 KB History raw

takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4 476168bb0 · Sergio ·

1#!/usr/bin/env bash
2# respaldo-promover.sh — mueve lo que el worker depositó en el BUZÓN al respaldo consolidado.
3#
4# ── POR QUÉ HAY UN BUZÓN Y NO ESCRITURA DIRECTA ────────────────────────────────────────────────
5# El worker es EFÍMERO y se borra solo; darle escritura sobre `takana/store` sería darle permiso de
6# borrado sobre el único respaldo que existe. Y los permisos de una subcuenta del Storage Box son
7# BINARIOS: `--readonly` sí o no. No hay append-only, no hay write-sin-delete (verificado en la API
8# el 2026-08-09, no de memoria).
9#
10# Así que la contención no se hace con un permiso sino con el ALCANCE: la subcuenta de escritura
11# tiene su home en `incoming/` y `takana/store` sencillamente NO EXISTE para ella. Lo máximo que
12# puede destruir es lo que ella misma depositó y todavía no se promovió — que sigue estando en el
13# volumen de la granja. Un permiso puede estar mal puesto; un directorio que no está en tu home no.
14#
15# La promoción es un `mv` DENTRO del mismo filesystem: un rename, instantáneo, cero bytes por la red.
16# Eso es lo que hace viable que el laptop dirija la operación con un uplink de 440 kB/s — manda
17# órdenes, no datos.
18#
19# ── LO QUE LA SHELL DEL BOX NO TIENE (medido) ──────────────────────────────────────────────────
20# Es una shell RESTRINGIDA, no un bash. Medido el 2026-08-09:
21#   · `for … do … done`  → «Command not found». No hay bucles: el batch se arma ACÁ.
22#   · `a; b`             → no encadena de fiar (la segunda orden no corrió). Una orden por conexión.
23#   · `mv a b c destino/`→ SÍ acepta varios orígenes ⇒ así se mueven cientos en una sola conexión.
24#   · `find`             → NO existe, y devuelve 0 EN SILENCIO. Un recuento con `find` acá parece
25#                          «no hay nada respaldado» y es mentira. Comparar con `ls` + `comm`.
26#
27# ── LA TRAMPA DEL `mv` SOBRE UN DESTINO QUE YA EXISTE ──────────────────────────────────────────
28# `mv A destino/` con `destino/A` ya existente NO falla: mete A DENTRO, y queda `destino/A/A`. Como
29# el store es CAS, un artefacto que ya está en el respaldo es idéntico al del buzón ⇒ no se mueve,
30# se BORRA del buzón. Por eso se parte en dos conjuntos antes de tocar nada.
31#
32# Uso:  scripts/farm/respaldo-promover.sh [--seco]
33set -uo pipefail
34ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
35KEY="${KEY:-$HOME/.ssh/github5}"
36MAIN="${MAIN:-u647150@u647150.your-storagebox.de}"
37BUZON="${BUZON:-incoming/store}"
38RESP="${RESP:-hammer/store}"
39LOTE="${LOTE:-80}"          # orígenes por conexión; el límite real es la longitud de la orden
40SECO=""; [ "${1:-}" = "--seco" ] && SECO=1
41
42sb() { ssh -4 -p 23 -i "$KEY" -o StrictHostKeyChecking=no -o ServerAliveInterval=30 "$MAIN" "$@"; }
43ART='^[0-9a-f]{64}-'         # sólo artefactos: `bootstrap.json` y demás metadatos NO se promueven
44
45TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
46sb "ls $BUZON" 2>/dev/null | grep -E "$ART" | sort -u > "$TMP/buzon.txt"
47sb "ls $RESP"  2>/dev/null | grep -E "$ART" | sort -u > "$TMP/resp.txt"
48N_BUZON=$(wc -l < "$TMP/buzon.txt"); N_RESP=$(wc -l < "$TMP/resp.txt")
49
50# Un listado vacío del RESPALDO es un fallo de red disfrazado de respaldo borrado. Si lo tomáramos
51# por bueno, todo el buzón parecería «nuevo» y lo moveríamos sobre un destino que quizá sí tiene
52# contenido. Ante la duda, no se toca nada.
53if [ "$N_RESP" -eq 0 ]; then
54  echo "⚠ el respaldo devolvió 0 artefactos — no promuevo (¿enlace caído?)" >&2; exit 1
55fi
56comm -23 "$TMP/buzon.txt" "$TMP/resp.txt" > "$TMP/nuevos.txt"
57comm -12 "$TMP/buzon.txt" "$TMP/resp.txt" > "$TMP/duplicados.txt"
58N_NUEVOS=$(wc -l < "$TMP/nuevos.txt"); N_DUP=$(wc -l < "$TMP/duplicados.txt")
59
60echo "==> buzón $N_BUZON · respaldo $N_RESP  ⇒  nuevos $N_NUEVOS · ya estaban $N_DUP"
61[ "$N_BUZON" -eq 0 ] && { echo "    buzón vacío, nada que hacer"; exit 0; }
62[ -n "$SECO" ] && { echo "    (--seco: no toco nada)"; exit 0; }
63
64# Los lotes se arman ACÁ (el box no sabe iterar) y viajan como argumentos de UNA orden por conexión.
65# Cada nombre es `<64 hex>-<paquete>`: sin espacios ni metacaracteres por construcción, así que
66# concatenarlos es seguro — y aun así se filtra por `$ART` antes de llegar hasta acá.
67en_lotes() {  # $1 = fichero de nombres, $2 = orden ("mv"|"rm")
68  lote=""; n=0; fallos=0
69  while read -r d; do
70    lote="$lote $BUZON/$d"; n=$((n + 1))
71    if [ "$n" -ge "$LOTE" ]; then
72      case "$2" in
73        mv) sb "mv$lote $RESP/" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
74        rm) sb "rm -rf$lote"    >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
75      esac
76      lote=""; n=0; printf '.'
77    fi
78  done < "$1"
79  if [ -n "$lote" ]; then
80    case "$2" in
81      mv) sb "mv$lote $RESP/" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
82      rm) sb "rm -rf$lote"    >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
83    esac
84    printf '.'
85  fi
86  echo " ($2: $fallos lote(s) con error)"
87}
88[ "$N_NUEVOS" -gt 0 ] && { printf '    promoviendo '; en_lotes "$TMP/nuevos.txt" mv; }
89[ "$N_DUP"    -gt 0 ] && { printf '    limpiando duplicados '; en_lotes "$TMP/duplicados.txt" rm; }
90
91# ── EL GUARDIÁN: RECONTAR, NO CREER ────────────────────────────────────────────────────────────
92# «moví N» no es lo mismo que «hay N más». La misma regla que salvó la poda del store.
93sb "ls $RESP"  2>/dev/null | grep -cE "$ART" > "$TMP/n2" || true
94sb "ls $BUZON" 2>/dev/null | grep -cE "$ART" > "$TMP/b2" || true
95N_RESP2=$(cat "$TMP/n2"); N_BUZON2=$(cat "$TMP/b2")
96echo "==> respaldo $N_RESP → $N_RESP2 (esperado $((N_RESP + N_NUEVOS)))  ·  buzón $N_BUZON → $N_BUZON2 (esperado 0)"
97RC=0
98[ "$N_RESP2" -eq "$((N_RESP + N_NUEVOS))" ] || { echo "⚠ el respaldo NO cuadra"; RC=1; }
99[ "$N_BUZON2" -eq 0 ] || { echo "⚠ el buzón no quedó vacío ($N_BUZON2)"; RC=1; }
100[ "$RC" -eq 0 ] && echo "GUARDIÁN: el recuento CASA ✓"
101
102# El manifiesto que hace honesto al grafo de estado (ver la cabecera de build-state.py).
103scripts/respaldo-storagebox.sh --listar || true
104exit $RC