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 1182Shell 87Rust 63Markdown 541592 Definitions

⎇ 965c41f4b18513e6c658a37753ffb770b600155f scripts/install-particion-test.sh Shell · 228 lines · 14.4 KB History raw

el instalador deja de ser «para esta máquina»: censo del terreno, menú y cmdline 59d444f0c · Sergio ·

1#!/bin/sh
2# install-particion-test.sh — banco de pruebas de `takana-install-partition.sh` sobre un disco REAL
3# (loop device) que trae vecinos: una ESP con Windows simulado y una partición con datos del usuario.
4#
5# ── QUÉ PRUEBA, Y POR QUÉ ASÍ ──────────────────────────────────────────────────────────────────
6# Este instalador se define por lo que NO hace, y eso no se valida leyendo su salida: se valida
7# comprobando que los BYTES de al lado siguen ahí. Por eso el test no mira mensajes, mira hashes:
8#
9#   1. la partición vecina queda idéntica byte a byte (sha256 del device entero, antes y después)
10#   2. la tabla de particiones queda idéntica (`sfdisk -d`)
11#   3. el `\EFI\BOOT\BOOTX64.EFI` y el `\EFI\Microsoft\` del vecino no se tocan
12#   4. nuestro kernel llega entero a `\EFI\takana\takanax64.efi` (mismo sha256 que el bzImage)
13#   5. las tres GUARDAS disparan: disco entero, partición montada, partición = ESP
14#   6. el guardián de capacidad NO se cree al filesystem: una ESP cuyo FAT declara más espacio del
15#      que la partición tiene (pasa de verdad con ESPs encogidas sin reformatear) se cota al tamaño
16#      real. El `df` del kernel repite la mentira, así que creerle al FS no alcanza.
17#
18# Requiere root (losetup + mount). Usa las herramientas SELLADAS del store, no las del host.
19#   sudo scripts/install-particion-test.sh
20set -eu
21ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
22[ "$(id -u)" = "0" ] || { echo "este test necesita root (losetup, mount)" >&2; exit 2; }
23
24P="${P:-work/.install-particion-test}"
25ROOTFS="${ROOTFS:-}"
26KERNEL="${KERNEL:-$(ls -dt /store/*-linux-metal/boot/bzImage store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
27[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro un bzImage de linux-metal (set KERNEL=)" >&2; exit 1; }
28if [ -z "$ROOTFS" ]; then
29    # ⚠ El rootfs tiene que estar en el MISMO filesystem que el stage: el armado usa `cp -al`
30    # (hardlinks) y `linkat()` no cruza mounts (EXDEV). En el hub el store está en otro disco, así
31    # que se copia una vez acá. No es un capricho del test: es el mismo motivo por el que
32    # install-image-efi.sh trae la `sonda_hardlink`.
33    _src=$(ls -dt /store/*-product-rootfs store/*-product-rootfs 2>/dev/null | head -1)
34    [ -n "${_src:-}" ] || { echo "no encuentro un product-rootfs (set ROOTFS=)" >&2; exit 1; }
35    ROOTFS="$P/rootfs"
36    [ -d "$ROOTFS" ] || { mkdir -p "$P"; echo "==> copiando $_src → $ROOTFS (una vez, por EXDEV)"; cp -a "$_src" "$ROOTFS"; }
37fi
38
39. "$(dirname "$0")/lib/herramientas-desde-store.sh"
40SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk)  || exit 1
41MKE2FS=$(herramienta e2fsprogs  sbin/mke2fs mke2fs)  || exit 1
42MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
43MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
44MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
45
46mkdir -p "$P"
47D="$P/disco.img"; LOOP=""
48limpiar() { [ -n "$LOOP" ] && losetup -d "$LOOP" 2>/dev/null || true; }
49trap limpiar EXIT INT TERM
50FALLOS=0
51ok()   { echo "  ✓ $*"; }
52falla(){ echo "  ✗ $*" >&2; FALLOS=$(( FALLOS + 1 )); }
53
54# ── el disco de mentira, con vecinos de verdad ────────────────────────────────────────────────
55# ESP_SANA=0 formatea la ESP con el tamaño del FICHERO ENTERO (el FAT miente sobre su capacidad);
56# es el escenario del punto 6. Con 1, la ESP se formatea aparte y se empalma en su offset — que es
57# la forma correcta, y la que usa install-image-efi.sh.
58ESP_SANA="${ESP_SANA:-1}"
59echo "==> fabricando $D (GPT: ESP 64M + destino 1200M + datos 500M + ~3,2G SIN ASIGNAR) ESP_SANA=$ESP_SANA"
60# 5000 MiB (sparse, no ocupan en disco) para que quede un HUECO SIN ASIGNAR al final: es lo que
61# necesita el escenario de `--use-free-space`, que instala al lado en vez de encima.
62rm -f "$D"; truncate -s 5000M "$D"
63"$SFDISK" --quiet "$D" <<EOF
64label: gpt
65start=2048,    size=131072,  type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="EFI System Partition"
66start=133120,  size=2457600, type=linux, name="destino-takana"
67start=2590720, size=1024000, type=linux, name="datos-del-usuario"
68EOF
69head -c 3000000 /dev/urandom > "$P/ajeno.bin"
70if [ "$ESP_SANA" = "1" ]; then
71    rm -f "$P/esp.img"; truncate -s 64M "$P/esp.img"; _esp="$P/esp.img"
72else
73    _esp="$D@@1048576"
74fi
75"$MFORMAT" -F -v VECINO -i "$_esp" ::
76"$MMD"   -i "$_esp" ::/EFI ::/EFI/BOOT ::/EFI/Microsoft ::/EFI/Microsoft/Boot
77"$MCOPY" -i "$_esp" "$P/ajeno.bin" ::/EFI/BOOT/BOOTX64.EFI
78"$MCOPY" -i "$_esp" "$P/ajeno.bin" ::/EFI/Microsoft/Boot/bootmgfw.efi
79[ "$ESP_SANA" = "1" ] && dd if="$P/esp.img" of="$D" bs=1M seek=1 conv=notrunc status=none
80rm -rf "$P/datos"; mkdir -p "$P/datos"; echo "la tesis del usuario, 2026" > "$P/datos/NO-BORRAR.txt"
81rm -f "$P/p3.img"; "$MKE2FS" -q -t ext4 -L datos-usuario -d "$P/datos" "$P/p3.img" 500M
82dd if="$P/p3.img" of="$D" bs=512 seek=2590720 conv=notrunc status=none
83
84LOOP=$(losetup -P -f --show "$D")
85echo "==> $D → $LOOP (p1=ESP p2=destino p3=datos)"
86"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-antes"
87
88# ── las guardas: cada una tiene que ABORTAR ───────────────────────────────────────────────────
89echo "==> guardas (las tres tienen que abortar SIN escribir)"
90corre() { ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" ./scripts/takana-install-partition.sh "$@" >/dev/null 2>&1; }
91corre --root-part "$LOOP" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó el DISCO ENTERO" || ok "rechaza el disco entero"
92mkdir -p "$P/mnt"; mount "${LOOP}p3" "$P/mnt"
93corre --root-part "${LOOP}p3" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó una partición MONTADA" || ok "rechaza una partición montada"
94umount "$P/mnt"
95corre --root-part "${LOOP}p1" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó root-part = ESP" || ok "rechaza root-part = ESP"
96
97# ── la instalación de verdad ──────────────────────────────────────────────────────────────────
98# ⚠ La huella de la vecina se toma ACÁ y no antes de las guardas, y la primera corrida de este test
99# explicó por qué: **montar un ext4 lo MODIFICA** (el superbloque guarda el último montaje y marca
100# el fs como montado), así que el `mount`/`umount` de la guarda de «partición montada» ya cambia el
101# sha256 del device. Medirlo antes daba un ROJO que no era del instalador — un test que acusa al
102# inocente se desactiva a la semana, que es peor que no tenerlo. Lo que este test tiene que probar
103# es que **la instalación** no la toca, y eso es exactamente lo que mide desde acá.
104sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes"
105# ⚠ Se instala por **PARTUUID**, no por `/dev/…`, y el banco lo hace así a propósito: los nombres
106# `nvmeXn1` NO son estables entre arranques (medido en QEMU con dos NVMe: el disco declarado primero
107# apareció como el segundo, y al rearrancar se cruzaron otra vez). Un instalador que confía en el
108# nombre puede formatear el disco equivocado. Si esta parte del test se cambia a un device suelto,
109# se pierde justamente la cobertura que importa.
110_pu=$("$SFDISK" -d "$LOOP" 2>/dev/null | awk -v d="${LOOP}p2" '$1==d {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i}}')
111[ -n "$_pu" ] && ok "PARTUUID del destino: $_pu" || falla "no pude leer el PARTUUID de ${LOOP}p2"
112echo "==> instalando en PARTUUID=$_pu (que hoy es ${LOOP}p2)"
113ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \
114    ./scripts/takana-install-partition.sh --root-part "PARTUUID=$_pu" --esp "${LOOP}p1" --no-nvram --yes 2>"$P/stderr" | tail -4
115if [ "$ESP_SANA" = "0" ]; then
116    grep -q "declara más espacio del que existe" "$P/stderr" \
117        && ok "el guardián de capacidad NO se cree al FAT mentiroso" \
118        || falla "el FAT mentiroso pasó sin advertencia"
119fi
120
121# ── lo que hay que comprobar es lo que NO cambió ──────────────────────────────────────────────
122echo "==> veredicto"
123[ "$(sha256sum "${LOOP}p3" | cut -d' ' -f1)" = "$(cat "$P/p3-antes")" ] \
124    && ok "la partición del usuario quedó idéntica byte a byte" || falla "LA PARTICIÓN DEL USUARIO CAMBIÓ"
125"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-ahora"
126cmp -s "$P/tabla-antes" "$P/tabla-ahora" && ok "la tabla de particiones no se tocó" || falla "LA TABLA CAMBIÓ"
127mkdir -p "$P/esp"; mount -t vfat "${LOOP}p1" "$P/esp"
128_a=$(sha256sum "$P/ajeno.bin" | cut -d' ' -f1)
129[ "$(sha256sum "$P/esp/EFI/BOOT/BOOTX64.EFI" | cut -d' ' -f1)" = "$_a" ] \
130    && ok "\\EFI\\BOOT\\BOOTX64.EFI del vecino intacto" || falla "SE PISÓ EL FALLBACK DEL VECINO"
131[ "$(sha256sum "$P/esp/EFI/Microsoft/Boot/bootmgfw.efi" | cut -d' ' -f1)" = "$_a" ] \
132    && ok "\\EFI\\Microsoft\\ intacto" || falla "SE TOCÓ EL WINDOWS DEL VECINO"
133[ "$(sha256sum "$P/esp/EFI/takana/takanax64.efi" | cut -d' ' -f1)" = "$(sha256sum "$KERNEL" | cut -d' ' -f1)" ] \
134    && ok "nuestro kernel llegó entero a \\EFI\\takana\\" || falla "el kernel de la ESP no coincide con el bzImage"
135[ -r "$P/esp/initramfs.cpio.gz" ] && ok "el initramfs de pivote está en la raíz de la ESP" || falla "falta \\initramfs.cpio.gz"
136umount "$P/esp"
137_lbl=$(blkid -s LABEL -o value "${LOOP}p2" 2>/dev/null || true)
138[ "$_lbl" = "hammer-root" ] && ok "el destino quedó ext4 LABEL=hammer-root" || falla "LABEL del destino = '${_lbl:-}' (esperaba hammer-root)"
139mkdir -p "$P/dst"; mount "${LOOP}p2" "$P/dst"
140[ -x "$P/dst/sbin/init" ] && ok "/sbin/init instalado (el wrapper que reconcilia el arranque)" || falla "el destino no tiene /sbin/init"
141umount "$P/dst"
142# Y el contenido del vecino, no sólo su hash: un fichero que sigue ahí y se lee es la prueba que el
143# usuario entiende.
144mount "${LOOP}p3" "$P/mnt"
145[ "$(cat "$P/mnt/NO-BORRAR.txt" 2>/dev/null)" = "la tesis del usuario, 2026" ] \
146    && ok "el fichero del usuario se sigue leyendo en la partición vecina" || falla "SE PERDIÓ EL FICHERO DEL USUARIO"
147umount "$P/mnt"
148
149# ── escenario 2: instalar AL LADO, en el hueco sin asignar ────────────────────────────────────
150# Es el camino al que cae la política de la raíz viva cuando la máquina no puede kexec, así que
151# tiene que estar igual de probado. Lo que se comprueba es que la tabla crece en UNA entrada y que
152# las que ya estaban no se mueven ni un sector.
153echo
154echo "==> escenario 2: --use-free-space (instalar al lado, en el hueco)"
155"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-antes2"
156_n_antes=$(wc -l < "$P/tabla-antes2")
157sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes2"
158ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \
159    ./scripts/takana-install-partition.sh --use-free-space --disk "$LOOP" --esp "${LOOP}p1" \
160    --no-nvram --yes 2>"$P/stderr2" | tail -3
161"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-ahora2"
162_n_ahora=$(wc -l < "$P/tabla-ahora2")
163[ "$_n_ahora" = "$(( _n_antes + 1 ))" ] && ok "la tabla creció en UNA entrada ($_n_antes → $_n_ahora)" \
164    || falla "la tabla pasó de $_n_antes a $_n_ahora entradas"
165head -n "$_n_antes" "$P/tabla-ahora2" > "$P/tabla-viejas2"
166cmp -s "$P/tabla-antes2" "$P/tabla-viejas2" && ok "las particiones que ya estaban no se movieron" \
167    || falla "SE MODIFICÓ ALGUNA ENTRADA EXISTENTE"
168[ "$(sha256sum "${LOOP}p3" | cut -d' ' -f1)" = "$(cat "$P/p3-antes2")" ] \
169    && ok "los datos del usuario siguen idénticos tras instalar al lado" || falla "LA VECINA CAMBIÓ AL INSTALAR AL LADO"
170_nueva=$(tail -1 "$P/tabla-ahora2" | cut -d' ' -f1 | tr -d ':')
171[ "$(blkid -s LABEL -o value "$_nueva" 2>/dev/null)" = "hammer-root" ] \
172    && ok "la partición nueva ($_nueva) quedó ext4 LABEL=hammer-root" || falla "la partición nueva no tiene el filesystem esperado"
173
174# ── escenario 3: el MODO INTERACTIVO, bajo un pty ─────────────────────────────────────────────
175# El menú sólo aparece si el instalador ve una terminal de verdad (`[ -t 0 ]`), así que no se puede
176# probar con un simple pipe: haría falta una tty. Se le da una con `python3 -m pty`, que es la forma
177# de automatizar exactamente el camino que recorre una persona. Se comprueba (1) que el censo
178# clasifica bien —que lo cifrado y lo de Windows NO se ofrecen— y (2) que la elección se convierte
179# en un PARTUUID y no en un nombre de device.
180echo
181echo "==> escenario 3: modo interactivo (censo + elección) bajo pty"
182_n_destino=$("$SFDISK" -d "$LOOP" 2>/dev/null | awk '/^\/dev/{n++} END{print n}')
183_menu_out="$P/menu.txt"
184ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" TAKANA_TEST_MENU=1 \
185python3 - "$PWD" "$LOOP" > "$_menu_out" 2>&1 <<'PYEOF' || true
186import os, pty, sys, time
187cwd, loop = sys.argv[1], sys.argv[2]
188# Se responde "no": lo que se valida es el CENSO y las opciones ofrecidas, no otra instalación.
189salida = []
190def leer(fd):
191    d = os.read(fd, 1024)
192    salida.append(d)
193    return d
194pid, fd = pty.fork()
195if pid == 0:
196    os.chdir(cwd)
197    os.execv("/bin/sh", ["sh", "scripts/takana-install-partition.sh"])
198buf = b""
199t0 = time.time()
200while time.time() - t0 < 120:
201    try:
202        d = leer(fd)
203    except OSError:
204        break
205    if not d:
206        break
207    buf += d
208    if b"salir sin tocar nada" in buf:
209        os.write(fd, b"no\n")
210        break
211try:
212    while True:
213        d = leer(fd)
214        if not d: break
215except OSError:
216    pass
217os.waitpid(pid, 0)
218sys.stdout.write(b"".join(salida).decode("utf-8", "replace"))
219PYEOF
220grep -q "el terreno de esta" "$_menu_out" && ok "el menú muestra el censo del terreno" || falla "el modo interactivo no mostró el censo"
221grep -q "opciones:" "$_menu_out" && ok "el menú ofrece opciones numeradas" || falla "el menú no ofreció opciones"
222grep -qi "NO TOCAR" "$_menu_out" && ok "hay particiones marcadas NO TOCAR" || falla "el censo no marcó ninguna partición como intocable"
223grep -q "PARTUUID=" "$_menu_out" && ok "el censo publica el PARTUUID de cada partición" || falla "el censo no muestra PARTUUID"
224grep -q "cancelado: no se tocó nada" "$_menu_out" && ok "responder «no» sale sin tocar nada" || falla "la salida por «no» no se comportó"
225
226echo
227[ "$FALLOS" = "0" ] && { echo "VERDE: instaló sin tocar nada de lo ajeno ($LOOP)"; exit 0; }
228echo "ROJO: $FALLOS comprobación(es) fallaron" >&2; exit 1