gioser

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 1531Shell 194Rust 108Markdown 983678 Definiciones

⎇ cc04b37f8ec92fbbb64e45e4e4065d0db2e087a5 scripts/metal-salto-test.sh Shell · 272 líneas · 15.8 KB Historia crudo

takana-os.net/descargas: UN fichero que se baja y sabe hacer todo cc04b37f8 · 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    TAKANA_TMP=/var/tmp /takana-instalador.run --root "$DEST" --esp "$ESPP" \
172        --unattended --yes > /dev/console 2>&1
173else
174    log "SALTO-TEST: ───── takana metal check ─────"
175    /bin/takana metal check > /dev/console 2>&1; log "SALTO-TEST: check rc=$?"
176    log "SALTO-TEST: ───── takana metal survey ─────"
177    /bin/takana metal survey --payload /paquete > /dev/console 2>&1; log "SALTO-TEST: survey rc=$?"
178    log "SALTO-TEST: ───── takana metal install ─────"
179    /bin/takana metal install --payload /paquete --root "$DEST" --esp "$ESPP" \
180        --unattended --yes > /dev/console 2>&1
181fi
182log "SALTO-TEST-FALLO: el salto NO ocurrió (install rc=$?) — el ORIGEN sigue vivo"
183sync; sleep 3; reboot -f
184INIT
185chmod 0755 "$T/init"
186( cd "$T" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$P/origen.cpio.gz"
187echo "    origen.cpio.gz $(du -m "$P/origen.cpio.gz" | cut -f1) MiB"
188
189# ── 3) QEMU en el worker ──────────────────────────────────────────────────────────────────────
190echo "==> subiendo al worker ($WORKER)"
191ssh -o BatchMode=yes "root@$WORKER" "mkdir -p $REMOTO"
192scp -q -o BatchMode=yes "$KEXEC_BZ" "$P/origen.cpio.gz" "root@$WORKER:$REMOTO/"
193# El disco viaja COMPRIMIDO. Son 4 GiB de los cuales ~3,9 son ceros: por `scp` tal cual tardó 7
194# minutos, gzipeado son segundos. Medido en la primera corrida de este arnés.
195"$GZIP" -1 -c "$D" | ssh -o BatchMode=yes "root@$WORKER" "gzip -d -c > $REMOTO/disco.img"
196echo "==> arrancando la VM (kernel del ORIGEN = linux-metal-kexec)"
197LOG="$P/serie.log"
198ssh -o BatchMode=yes "root@$WORKER" "cd $REMOTO && timeout 900 qemu-system-x86_64 \
199      -m 4096 -smp 2 -display none -vga std -serial mon:stdio -no-reboot \
200      -kernel bzImage -initrd origen.cpio.gz \
201      -append 'console=tty0 console=ttyS0,115200 test.root=PARTUUID=$PU_ROOT test.esp=PARTUUID=$PU_ESP' \
202      -drive file=disco.img,format=raw,if=virtio </dev/null" > "$LOG" 2>&1 || true
203echo "    log: $LOG ($(wc -l < "$LOG") líneas)"
204echo "==> trayendo el disco de vuelta"
205ssh -o BatchMode=yes "root@$WORKER" "gzip -c $REMOTO/disco.img" > "$P/disco-despues.img.gz"
206[ -n "$CONSERVAR" ] || ssh -o BatchMode=yes "root@$WORKER" "rm -rf $REMOTO"
207"$GZIP" -d -c "$P/disco-despues.img.gz" > "$P/disco-despues.img"
208fi
209LOG="$P/serie.log"
210SHA_VECINO=$(sha256sum "$P/ajeno.bin" | cut -d' ' -f1)
211
212# ── 4) el veredicto sale del DISCO, no de la pantalla ─────────────────────────────────────────
213echo "==> comprobaciones"
214A="$P/disco-despues.img"
215grep -q 'SALTO-TEST: sistema ORIGEN vivo' "$LOG" && ok "el sistema origen arrancó" || falla "el origen no arrancó"
216grep -q 'puede instalarse SIN USB' "$LOG" && ok "metal check: dice que SÍ (el kernel trae el flag)" \
217    || falla "metal check no reconoció el kexec en un kernel que lo trae"
218grep -q 'el terreno de esta máquina' "$LOG" && ok "metal survey: el censo salió por pantalla" \
219    || falla "metal survey no imprimió el censo"
220if [ -n "$VIA_RUN" ]; then
221    grep -q 'payload verificado (sha256)' "$LOG" && ok "el .run verificó su propio payload" \
222        || falla "el .run no verificó el sha256 de su payload"
223    grep -q 'desempaquetando' "$LOG" && ok "el .run se desempaquetó solo" || falla "el .run no se desempaquetó"
224fi
225grep -q 'saltando al instalador' "$LOG" && ok "metal install: anunció el salto" || falla "no anunció el salto"
226grep -q 'SALTO-TEST-FALLO' "$LOG" && falla "EL SALTO NO OCURRIÓ: el origen siguió vivo" \
227    || ok "EL SALTO OCURRIÓ: el origen no volvió del kexec"
228grep -q 'TAKANA-INSTALADOR: arrancado en RAM' "$LOG" && ok "el instalador arrancó DESPUÉS del salto" \
229    || falla "el instalador no arrancó tras el salto"
230grep -q 'TAKANA-INSTALADOR-OK' "$LOG" && ok "el instalador terminó bien" || falla "el instalador no llegó al final"
231
232# La tabla: tenía que quedar exactamente igual (este camino formatea, no reparticiona).
233# ⚠ Dos trampas, las dos medidas en la primera corrida: (a) `sfdisk -d` de un FICHERO prefija cada
234# línea con la RUTA del fichero, y las dos rutas son distintas por construcción —el antes y el
235# después no son el mismo fichero—, así que hay que quitar esa columna o el test es rojo siempre;
236# (b) `cmp` NO EXISTE en este anfitrión musl, y su ausencia no rompía el test: `cmp: command not
237# found` sale 127, el `if` lo toma como falso y el test acusaba «la tabla cambió» sin haber
238# comparado nada. Un test que no puede correr tiene que decirlo, no fallar con el mensaje de otra
239# cosa. Se compara con el propio shell.
240tabla_de() { "$SFDISK" -d "$1" 2>/dev/null | grep 'start=' | sed 's/^[^:]*:/:/'; }
241tabla_de "$A" > "$P/tabla-despues"; tabla_de "$D" > "$P/tabla-antes"
242if [ ! -s "$P/tabla-antes" ]; then falla "no pude leer la tabla del disco original: no comparé nada"
243elif [ "$(cat "$P/tabla-antes")" = "$(cat "$P/tabla-despues")" ]; then ok "la tabla de particiones NO cambió"
244else falla "la tabla cambió"; fi
245
246# El destino: ext4 con la etiqueta que busca el initramfs de pivote. Se extrae por OFFSET, que es
247# lo único que se puede hacer sin loop devices dentro de esta jaula.
248OFF=$(( 264192 * 512 ))
249dd if="$A" of="$P/p2.img" bs=1M skip=$(( OFF / 1048576 )) count=2400 status=none
250if "$BLKID" "$P/p2.img" 2>/dev/null | grep -q 'LABEL="hammer-root"'; then
251    ok "el destino quedó ext4 con LABEL=hammer-root"
252else
253    falla "el destino no tiene LABEL=hammer-root: $("$BLKID" "$P/p2.img" 2>&1 | head -1)"
254fi
255
256# La ESP: nuestra carpeta de vendor puesta, y la del vecino intacta.
257ESPOFF="$A@@1048576"
258if "$MDIR" -i "$ESPOFF" ::/EFI/takana 2>/dev/null | grep -qi 'takanax64'; then
259    ok "la ESP tiene \\EFI\\takana\\takanax64.efi"
260else
261    falla "la ESP no tiene nuestra carpeta de vendor"
262fi
263rm -f "$P/vecino-despues.bin"
264"$MCOPY" -i "$ESPOFF" ::/EFI/Microsoft/Boot/bootmgfw.efi "$P/vecino-despues.bin" 2>/dev/null || true
265SHA_DESPUES=$(sha256sum "$P/vecino-despues.bin" 2>/dev/null | cut -d' ' -f1)
266[ -n "$SHA_DESPUES" ] && [ "$SHA_VECINO" = "$SHA_DESPUES" ] \
267    && ok "el vecino de la ESP (bootmgfw.efi de Microsoft) quedó byte a byte igual" \
268    || falla "el vecino de la ESP cambió o desapareció"
269
270echo
271if [ "$FALLOS" = 0 ]; then echo "VERDE — el salto se ejecutó y el disco quedó instalado."; else
272    echo "ROJO — $FALLOS comprobación(es) fallaron. Log: $LOG" >&2; exit 1; fi