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 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 36152da9284ad15039abe44e3cb91ef42aa43bb7 scripts/metal-salto-test.sh Shell · 274 siq'ikuna · 16.0 KB Ñawpa willay ch'uya

primero se elige, después se salta — y sólo si hace falta (SDD 32) 36152da92 · Sergio ·

1#!/bin/sh
2# metal-salto-test.sh — el SALTO, de verdad: un Linux vivo se reemplaza a sí mismo sin USB.
3#
4# ══ QUÉ PRUEBA, Y POR QUÉ HACÍA FALTA ══════════════════════════════════════════════════════════
5# Del camino «instalar sin USB» estaba probado TODO menos el salto: el instalador en RAM se había
6# arrancado a mano y hacía su trabajo, pero `kexec_file_load` no se había ejecutado NUNCA — ninguna
7# de nuestras máquinas corre un kernel con `CONFIG_KEXEC_FILE`, así que el primer sitio donde el
8# salto iba a pasar era la máquina del usuario. Eso convierte al usuario en el banco de pruebas.
9#
10# Este arnés lo cierra: arranca una VM con `linux-metal-kexec` —la variante que SÍ trae el flag—,
11# le da un disco con vecinos de verdad y corre ahí el verbo de usuario:
12#
13#     takana metal check  →  takana metal survey  →  takana metal install
14#
15# Lo que tiene que pasar: el sistema ORIGEN muere en el salto, el instalador arranca en RAM,
16# formatea SÓLO su destino y deja el disco arrancable. Se comprueba sobre el DISCO, no sobre lo que
17# la pantalla dijo — la lección de `tee`: un pipe alegre imprimía «instalado» sin haber formateado.
18#
19# ⚠ **Corre en el WORKER** (`dev.gioser.net`, el LXC prestado): es la única máquina con QEMU. El
20# disco vuelve al hub y se verifica acá, con las herramientas selladas.
21#
22# Uso:  scripts/metal-salto-test.sh [--conservar]
23# Vars: WORKER (def dev.gioser.net) · P (dir de trabajo, def work/.salto-test) · PAQUETE
24set -eu
25ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
26
27WORKER="${WORKER:-dev.gioser.net}"
28P="${P:-work/.salto-test}"
29PAQUETE="${PAQUETE:-work/instalador-salida}"
30REMOTO="/tmp/salto-test-$$"
31CONSERVAR=""; SOLO_VERIFICAR=""
32# ⚠ `VIA_RUN=1` prueba **el fichero que se publica**, no el paquete. Son cosas distintas: el paquete
33# es lo que arma el repo y el `.run` es lo que baja una persona, y entre uno y otro hay una cabecera
34# `sh`, un offset de bytes, una verificación de sha256 y un desempaquetado — o sea cuatro sitios
35# nuevos donde romperse, ninguno cubierto por probar el paquete.
36VIA_RUN="${VIA_RUN:-}"
37for a in "$@"; do
38    case "$a" in
39        --conservar) CONSERVAR=1 ;;
40        # Reusar el disco que ya volvió de la VM. La corrida entera son ~10 min de los cuales 9 son
41        # mover 4 GiB; iterar sobre las COMPROBACIONES no tiene por qué pagarlos otra vez.
42        --verificar-solo) SOLO_VERIFICAR=1 ;;
43        *) echo "opción desconocida: $a" >&2; exit 2 ;;
44    esac
45done
46
47. "$(dirname "$0")/lib/herramientas-desde-store.sh"
48SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk)   || exit 1
49BLKID=$(herramienta  util-linux sbin/blkid blkid)     || exit 1
50MKE2FS=$(herramienta e2fsprogs  sbin/mke2fs mke2fs)   || exit 1
51MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
52MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
53MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
54MDIR=$(herramienta    mtools usr/bin/mdir    mdir)    || exit 1
55CPIO=$(herramienta    libarchive usr/bin/bsdcpio cpio) || exit 1
56GZIP=$(herramienta    gzip bin/gzip gzip)             || exit 1
57
58FALLOS=0
59ok()    { echo "  ✓ $*"; }
60falla() { echo "  ✗ $*" >&2; FALLOS=$(( FALLOS + 1 )); }
61
62# El kernel del ORIGEN es el único que tiene que traer CONFIG_KEXEC_FILE: el del instalador puede
63# ser cualquiera, porque ya no salta a ningún lado.
64KEXEC_ART=$(ls -d /store/*-linux-metal-kexec store/*-linux-metal-kexec 2>/dev/null | head -1)
65[ -n "${KEXEC_ART:-}" ] || { echo "no encuentro el artefacto linux-metal-kexec en el store" >&2; exit 1; }
66KEXEC_BZ="$KEXEC_ART/boot/bzImage"
67grep -q '^CONFIG_KEXEC_FILE=y' "$KEXEC_ART"/boot/config-* || {
68    echo "el kernel del origen NO trae CONFIG_KEXEC_FILE — el test no probaría nada" >&2; exit 1; }
69[ -r "$PAQUETE/takana-instalador.cpio.gz" ] && [ -r "$PAQUETE/kernel/boot/bzImage" ] || {
70    echo "el paquete $PAQUETE está incompleto (armalo con scripts/instalador-initramfs.sh)" >&2; exit 1; }
71
72mkdir -p "$P"
73D="$P/disco.img"
74if [ -n "$SOLO_VERIFICAR" ]; then
75    [ -r "$P/disco-despues.img" ] && [ -r "$P/serie.log" ] || {
76        echo "--verificar-solo: no hay una corrida previa en $P" >&2; exit 2; }
77    echo "==> --verificar-solo: reusando $P/disco-despues.img y $P/serie.log"
78fi
79
80# ── 1) el disco: ESP con un vecino dentro, destino vacío, y un hueco sin asignar ───────────────
81if [ -z "$SOLO_VERIFICAR" ]; then
82echo "==> disco de prueba (GPT: ESP 128M + destino 2400M + ~1,5G sin asignar)"
83rm -f "$D"; truncate -s 4096M "$D"
84"$SFDISK" --quiet "$D" <<'EOF'
85label: gpt
86start=2048,   size=262144,  type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="EFI System Partition"
87start=264192, size=4915200, type=linux, name="destino-takana"
88EOF
89# La ESP se formatea APARTE y se empalma en su offset: formatearla in-situ con `@@` hace que el FAT
90# declare el tamaño del fichero entero, y después `df` repite la mentira (medido, ADR del banco).
91rm -f "$P/esp.img"; truncate -s 128M "$P/esp.img"
92"$MFORMAT" -F -v VECINO -i "$P/esp.img" ::
93"$MMD"   -i "$P/esp.img" ::/EFI ::/EFI/Microsoft ::/EFI/Microsoft/Boot
94head -c 400000 /dev/urandom > "$P/ajeno.bin"
95"$MCOPY" -i "$P/esp.img" "$P/ajeno.bin" ::/EFI/Microsoft/Boot/bootmgfw.efi
96dd if="$P/esp.img" of="$D" bs=1M seek=1 conv=notrunc status=none
97PU_ROOT=$("$SFDISK" -d "$D" | awk '/destino-takana/ {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i}}')
98PU_ESP=$("$SFDISK"  -d "$D" | awk '/EFI System/     {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i}}')
99[ -n "$PU_ROOT" ] && [ -n "$PU_ESP" ] || { echo "no pude leer los PARTUUID de la tabla" >&2; exit 1; }
100echo "    destino PARTUUID=$PU_ROOT · ESP PARTUUID=$PU_ESP"
101# La huella del vecino de la ESP es la del FICHERO, no la del listado. La primera versión de este
102# arnés comparaba `mdir` contra `mdir` y salía ROJO SIEMPRE: el listado trae el «bytes free» del
103# volumen, que cambia en cuanto se copia cualquier otra cosa en la ESP. Un test que acusa al
104# inocente se desactiva a la semana.
105SHA_VECINO=$(sha256sum "$P/ajeno.bin" | cut -d' ' -f1)
106
107# ── 2) el sistema ORIGEN: el Linux que se va a suicidar ───────────────────────────────────────
108# Es deliberadamente mínimo: lo único que tiene que hacer es correr el verbo de usuario. Si el
109# salto anda, nada de este árbol sobrevive.
110echo "==> initramfs del sistema ORIGEN"
111T="$P/origen"; rm -rf "$T"
112mkdir -p "$T/bin" "$T/dev" "$T/proc" "$T/sys" "$T/tmp" "$T/run" "$T/paquete/lib" "$T/paquete/kernel/boot"
113# Por HASH, no por `ls | head -1`: hay tres artefactos `-busybox` en este store y el primero que
114# lista el directorio no tiene por qué ser el vigente. La fecha miente; el hash no.
115BUSYBOX="${BUSYBOX:-$(herramienta busybox bin/busybox busybox)}" || exit 1
116[ -r "${BUSYBOX:-}" ] || { echo "no encuentro busybox" >&2; exit 1; }
117cp "$BUSYBOX" "$T/bin/busybox"; chmod 0755 "$T/bin/busybox"
118for a in sh mount umount mkdir echo sleep ls cat sed awk grep cut tr basename dirname readlink \
119         find sync reboot dmesg blkid setsid tee du wc head tail sort printf test id chmod rm cp \
120         mv stat findfs uname sha256sum; do
121    ln -sf busybox "$T/bin/$a"
122done
123cp "$SFDISK" "$T/bin/sfdisk"; chmod 0755 "$T/bin/sfdisk"
124TAKANA_BIN="${TAKANA_BIN:-$ROOT/target/x86_64-unknown-linux-musl/release/takana}"
125[ -x "$TAKANA_BIN" ] || TAKANA_BIN="$ROOT/target/release/takana"
126cp "$TAKANA_BIN" "$T/bin/takana"; chmod 0755 "$T/bin/takana"
127if [ -n "$VIA_RUN" ]; then
128    RUN="${RUN:-work/takana-instalador.run}"
129    [ -x "$RUN" ] || { echo "no encuentro $RUN (armalo con scripts/instalador-autoextraible.sh)" >&2; exit 1; }
130    mkdir -p "$T/var/tmp"
131    cp "$RUN" "$T/takana-instalador.run"; chmod 0755 "$T/takana-instalador.run"
132    for a in tar gzip gunzip df expr; do ln -sf busybox "$T/bin/$a"; done
133    rm -rf "$T/paquete"
134else
135    # El PAQUETE tal como lo recibe el usuario: el instalador, su kernel y el censo.
136    cp "$PAQUETE/takana-instalador.cpio.gz" "$T/paquete/"
137    cp "$PAQUETE/kernel/boot/bzImage"       "$T/paquete/kernel/boot/"
138    cp scripts/takana-install-partition.sh  "$T/paquete/"
139    cp scripts/lib/herramientas-desde-store.sh "$T/paquete/lib/"
140fi
141
142cat > "$T/init" <<'INIT'
143#!/bin/busybox sh
144# PID 1 del sistema ORIGEN. No instala nada: corre el verbo de usuario y deja de existir.
145export PATH=/bin
146mount -t proc proc /proc 2>/dev/null || true
147mount -t sysfs sys /sys  2>/dev/null || true
148mount -t devtmpfs dev /dev 2>/dev/null || true
149# Sin store adentro, el censo recibe sus herramientas por el override explícito.
150export TOOL_sfdisk=/bin/sfdisk
151log() { echo "$*" > /dev/console 2>/dev/null || true; }
152log "SALTO-TEST: sistema ORIGEN vivo — kernel $(uname -r), uid $(id -u)"
153i=0
154while [ "$i" -lt 30 ]; do
155    hay=""
156    for d in /dev/vd? /dev/sd? /dev/nvme?n?; do [ -b "$d" ] && hay=1; done
157    [ -n "$hay" ] && break
158    i=$(( i + 1 )); sleep 1
159done
160log "SALTO-TEST: discos: $(for d in /dev/vd? /dev/sd?; do [ -b "$d" ] && printf '%s ' "$d"; done)"
161DEST=""; ESPP=""
162for w in $(cat /proc/cmdline 2>/dev/null); do
163    case "$w" in
164        test.root=*) DEST="${w#test.root=}" ;;
165        test.esp=*)  ESPP="${w#test.esp=}" ;;
166    esac
167done
168if [ -x /takana-instalador.run ]; then
169    # El camino del que BAJA UN FICHERO: la cabecera se desempaqueta sola y corre lo de adentro.
170    log "SALTO-TEST: ───── takana-instalador.run (el fichero publicado) ─────"
171    # `--jump`: desde el 2026-09-23 el salto sólo ocurre si el destino es la raíz VIVA, y acá no
172    # lo es. Este arnés existe para probar el salto, así que lo pide explícitamente.
173    TAKANA_TMP=/var/tmp /takana-instalador.run --root "$DEST" --esp "$ESPP" \
174        --jump --unattended --yes > /dev/console 2>&1
175else
176    log "SALTO-TEST: ───── takana metal check ─────"
177    /bin/takana metal check > /dev/console 2>&1; log "SALTO-TEST: check rc=$?"
178    log "SALTO-TEST: ───── takana metal survey ─────"
179    /bin/takana metal survey --payload /paquete > /dev/console 2>&1; log "SALTO-TEST: survey rc=$?"
180    log "SALTO-TEST: ───── takana metal install ─────"
181    /bin/takana metal install --payload /paquete --root "$DEST" --esp "$ESPP" \
182        --jump --unattended --yes > /dev/console 2>&1
183fi
184log "SALTO-TEST-FALLO: el salto NO ocurrió (install rc=$?) — el ORIGEN sigue vivo"
185sync; sleep 3; reboot -f
186INIT
187chmod 0755 "$T/init"
188( cd "$T" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$P/origen.cpio.gz"
189echo "    origen.cpio.gz $(du -m "$P/origen.cpio.gz" | cut -f1) MiB"
190
191# ── 3) QEMU en el worker ──────────────────────────────────────────────────────────────────────
192echo "==> subiendo al worker ($WORKER)"
193ssh -o BatchMode=yes "root@$WORKER" "mkdir -p $REMOTO"
194scp -q -o BatchMode=yes "$KEXEC_BZ" "$P/origen.cpio.gz" "root@$WORKER:$REMOTO/"
195# El disco viaja COMPRIMIDO. Son 4 GiB de los cuales ~3,9 son ceros: por `scp` tal cual tardó 7
196# minutos, gzipeado son segundos. Medido en la primera corrida de este arnés.
197"$GZIP" -1 -c "$D" | ssh -o BatchMode=yes "root@$WORKER" "gzip -d -c > $REMOTO/disco.img"
198echo "==> arrancando la VM (kernel del ORIGEN = linux-metal-kexec)"
199LOG="$P/serie.log"
200ssh -o BatchMode=yes "root@$WORKER" "cd $REMOTO && timeout 900 qemu-system-x86_64 \
201      -m 4096 -smp 2 -display none -vga std -serial mon:stdio -no-reboot \
202      -kernel bzImage -initrd origen.cpio.gz \
203      -append 'console=tty0 console=ttyS0,115200 test.root=PARTUUID=$PU_ROOT test.esp=PARTUUID=$PU_ESP' \
204      -drive file=disco.img,format=raw,if=virtio </dev/null" > "$LOG" 2>&1 || true
205echo "    log: $LOG ($(wc -l < "$LOG") líneas)"
206echo "==> trayendo el disco de vuelta"
207ssh -o BatchMode=yes "root@$WORKER" "gzip -c $REMOTO/disco.img" > "$P/disco-despues.img.gz"
208[ -n "$CONSERVAR" ] || ssh -o BatchMode=yes "root@$WORKER" "rm -rf $REMOTO"
209"$GZIP" -d -c "$P/disco-despues.img.gz" > "$P/disco-despues.img"
210fi
211LOG="$P/serie.log"
212SHA_VECINO=$(sha256sum "$P/ajeno.bin" | cut -d' ' -f1)
213
214# ── 4) el veredicto sale del DISCO, no de la pantalla ─────────────────────────────────────────
215echo "==> comprobaciones"
216A="$P/disco-despues.img"
217grep -q 'SALTO-TEST: sistema ORIGEN vivo' "$LOG" && ok "el sistema origen arrancó" || falla "el origen no arrancó"
218grep -q 'puede instalarse SIN USB' "$LOG" && ok "metal check: dice que SÍ (el kernel trae el flag)" \
219    || falla "metal check no reconoció el kexec en un kernel que lo trae"
220grep -q 'el terreno de esta máquina' "$LOG" && ok "metal survey: el censo salió por pantalla" \
221    || falla "metal survey no imprimió el censo"
222if [ -n "$VIA_RUN" ]; then
223    grep -q 'payload verificado (sha256)' "$LOG" && ok "el .run verificó su propio payload" \
224        || falla "el .run no verificó el sha256 de su payload"
225    grep -q 'desempaquetando' "$LOG" && ok "el .run se desempaquetó solo" || falla "el .run no se desempaquetó"
226fi
227grep -q '⚡ saltando' "$LOG" && ok "metal install: anunció el salto" || falla "no anunció el salto"
228grep -q 'SALTO-TEST-FALLO' "$LOG" && falla "EL SALTO NO OCURRIÓ: el origen siguió vivo" \
229    || ok "EL SALTO OCURRIÓ: el origen no volvió del kexec"
230grep -q 'TAKANA-INSTALADOR: arrancado en RAM' "$LOG" && ok "el instalador arrancó DESPUÉS del salto" \
231    || falla "el instalador no arrancó tras el salto"
232grep -q 'TAKANA-INSTALADOR-OK' "$LOG" && ok "el instalador terminó bien" || falla "el instalador no llegó al final"
233
234# La tabla: tenía que quedar exactamente igual (este camino formatea, no reparticiona).
235# ⚠ Dos trampas, las dos medidas en la primera corrida: (a) `sfdisk -d` de un FICHERO prefija cada
236# línea con la RUTA del fichero, y las dos rutas son distintas por construcción —el antes y el
237# después no son el mismo fichero—, así que hay que quitar esa columna o el test es rojo siempre;
238# (b) `cmp` NO EXISTE en este anfitrión musl, y su ausencia no rompía el test: `cmp: command not
239# found` sale 127, el `if` lo toma como falso y el test acusaba «la tabla cambió» sin haber
240# comparado nada. Un test que no puede correr tiene que decirlo, no fallar con el mensaje de otra
241# cosa. Se compara con el propio shell.
242tabla_de() { "$SFDISK" -d "$1" 2>/dev/null | grep 'start=' | sed 's/^[^:]*:/:/'; }
243tabla_de "$A" > "$P/tabla-despues"; tabla_de "$D" > "$P/tabla-antes"
244if [ ! -s "$P/tabla-antes" ]; then falla "no pude leer la tabla del disco original: no comparé nada"
245elif [ "$(cat "$P/tabla-antes")" = "$(cat "$P/tabla-despues")" ]; then ok "la tabla de particiones NO cambió"
246else falla "la tabla cambió"; fi
247
248# El destino: ext4 con la etiqueta que busca el initramfs de pivote. Se extrae por OFFSET, que es
249# lo único que se puede hacer sin loop devices dentro de esta jaula.
250OFF=$(( 264192 * 512 ))
251dd if="$A" of="$P/p2.img" bs=1M skip=$(( OFF / 1048576 )) count=2400 status=none
252if "$BLKID" "$P/p2.img" 2>/dev/null | grep -q 'LABEL="hammer-root"'; then
253    ok "el destino quedó ext4 con LABEL=hammer-root"
254else
255    falla "el destino no tiene LABEL=hammer-root: $("$BLKID" "$P/p2.img" 2>&1 | head -1)"
256fi
257
258# La ESP: nuestra carpeta de vendor puesta, y la del vecino intacta.
259ESPOFF="$A@@1048576"
260if "$MDIR" -i "$ESPOFF" ::/EFI/takana 2>/dev/null | grep -qi 'takanax64'; then
261    ok "la ESP tiene \\EFI\\takana\\takanax64.efi"
262else
263    falla "la ESP no tiene nuestra carpeta de vendor"
264fi
265rm -f "$P/vecino-despues.bin"
266"$MCOPY" -i "$ESPOFF" ::/EFI/Microsoft/Boot/bootmgfw.efi "$P/vecino-despues.bin" 2>/dev/null || true
267SHA_DESPUES=$(sha256sum "$P/vecino-despues.bin" 2>/dev/null | cut -d' ' -f1)
268[ -n "$SHA_DESPUES" ] && [ "$SHA_VECINO" = "$SHA_DESPUES" ] \
269    && ok "el vecino de la ESP (bootmgfw.efi de Microsoft) quedó byte a byte igual" \
270    || falla "el vecino de la ESP cambió o desapareció"
271
272echo
273if [ "$FALLOS" = 0 ]; then echo "VERDE — el salto se ejecutó y el disco quedó instalado."; else
274    echo "ROJO — $FALLOS comprobación(es) fallaron. Log: $LOG" >&2; exit 1; fi