#!/bin/sh # install-particion-test.sh — banco de pruebas de `takana-install-partition.sh` sobre un disco REAL # (loop device) que trae vecinos: una ESP con Windows simulado y una partición con datos del usuario. # # ── QUÉ PRUEBA, Y POR QUÉ ASÍ ────────────────────────────────────────────────────────────────── # Este instalador se define por lo que NO hace, y eso no se valida leyendo su salida: se valida # comprobando que los BYTES de al lado siguen ahí. Por eso el test no mira mensajes, mira hashes: # # 1. la partición vecina queda idéntica byte a byte (sha256 del device entero, antes y después) # 2. la tabla de particiones queda idéntica (`sfdisk -d`) # 3. el `\EFI\BOOT\BOOTX64.EFI` y el `\EFI\Microsoft\` del vecino no se tocan # 4. nuestro kernel llega entero a `\EFI\takana\takanax64.efi` (mismo sha256 que el bzImage) # 5. las tres GUARDAS disparan: disco entero, partición montada, partición = ESP # 6. el guardián de capacidad NO se cree al filesystem: una ESP cuyo FAT declara más espacio del # que la partición tiene (pasa de verdad con ESPs encogidas sin reformatear) se cota al tamaño # real. El `df` del kernel repite la mentira, así que creerle al FS no alcanza. # # Requiere root (losetup + mount). Usa las herramientas SELLADAS del store, no las del host. # sudo scripts/install-particion-test.sh set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" [ "$(id -u)" = "0" ] || { echo "este test necesita root (losetup, mount)" >&2; exit 2; } P="${P:-work/.install-particion-test}" ROOTFS="${ROOTFS:-}" KERNEL="${KERNEL:-$(ls -dt /store/*-linux-metal/boot/bzImage store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}" [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro un bzImage de linux-metal (set KERNEL=)" >&2; exit 1; } if [ -z "$ROOTFS" ]; then # ⚠ El rootfs tiene que estar en el MISMO filesystem que el stage: el armado usa `cp -al` # (hardlinks) y `linkat()` no cruza mounts (EXDEV). En el hub el store está en otro disco, así # que se copia una vez acá. No es un capricho del test: es el mismo motivo por el que # install-image-efi.sh trae la `sonda_hardlink`. _src=$(ls -dt /store/*-product-rootfs store/*-product-rootfs 2>/dev/null | head -1) [ -n "${_src:-}" ] || { echo "no encuentro un product-rootfs (set ROOTFS=)" >&2; exit 1; } ROOTFS="$P/rootfs" [ -d "$ROOTFS" ] || { mkdir -p "$P"; echo "==> copiando $_src → $ROOTFS (una vez, por EXDEV)"; cp -a "$_src" "$ROOTFS"; } fi . "$(dirname "$0")/lib/herramientas-desde-store.sh" SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1 MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1 MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1 MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1 MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1 mkdir -p "$P" D="$P/disco.img"; LOOP="" limpiar() { [ -n "$LOOP" ] && losetup -d "$LOOP" 2>/dev/null || true; } trap limpiar EXIT INT TERM FALLOS=0 ok() { echo " ✓ $*"; } falla(){ echo " ✗ $*" >&2; FALLOS=$(( FALLOS + 1 )); } # ── el disco de mentira, con vecinos de verdad ──────────────────────────────────────────────── # ESP_SANA=0 formatea la ESP con el tamaño del FICHERO ENTERO (el FAT miente sobre su capacidad); # es el escenario del punto 6. Con 1, la ESP se formatea aparte y se empalma en su offset — que es # la forma correcta, y la que usa install-image-efi.sh. ESP_SANA="${ESP_SANA:-1}" echo "==> fabricando $D (GPT: ESP 64M + destino 1200M + datos 500M + ~3,2G SIN ASIGNAR) ESP_SANA=$ESP_SANA" # 5000 MiB (sparse, no ocupan en disco) para que quede un HUECO SIN ASIGNAR al final: es lo que # necesita el escenario de `--use-free-space`, que instala al lado en vez de encima. rm -f "$D"; truncate -s 5000M "$D" "$SFDISK" --quiet "$D" < "$P/ajeno.bin" if [ "$ESP_SANA" = "1" ]; then rm -f "$P/esp.img"; truncate -s 64M "$P/esp.img"; _esp="$P/esp.img" else _esp="$D@@1048576" fi "$MFORMAT" -F -v VECINO -i "$_esp" :: "$MMD" -i "$_esp" ::/EFI ::/EFI/BOOT ::/EFI/Microsoft ::/EFI/Microsoft/Boot "$MCOPY" -i "$_esp" "$P/ajeno.bin" ::/EFI/BOOT/BOOTX64.EFI "$MCOPY" -i "$_esp" "$P/ajeno.bin" ::/EFI/Microsoft/Boot/bootmgfw.efi [ "$ESP_SANA" = "1" ] && dd if="$P/esp.img" of="$D" bs=1M seek=1 conv=notrunc status=none rm -rf "$P/datos"; mkdir -p "$P/datos"; echo "la tesis del usuario, 2026" > "$P/datos/NO-BORRAR.txt" rm -f "$P/p3.img"; "$MKE2FS" -q -t ext4 -L datos-usuario -d "$P/datos" "$P/p3.img" 500M dd if="$P/p3.img" of="$D" bs=512 seek=2590720 conv=notrunc status=none LOOP=$(losetup -P -f --show "$D") echo "==> $D → $LOOP (p1=ESP p2=destino p3=datos)" "$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-antes" # ── las guardas: cada una tiene que ABORTAR ─────────────────────────────────────────────────── echo "==> guardas (las tres tienen que abortar SIN escribir)" corre() { ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" ./scripts/takana-install-partition.sh "$@" >/dev/null 2>&1; } corre --root-part "$LOOP" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó el DISCO ENTERO" || ok "rechaza el disco entero" mkdir -p "$P/mnt"; mount "${LOOP}p3" "$P/mnt" corre --root-part "${LOOP}p3" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó una partición MONTADA" || ok "rechaza una partición montada" umount "$P/mnt" corre --root-part "${LOOP}p1" --esp "${LOOP}p1" --no-nvram --yes && falla "aceptó root-part = ESP" || ok "rechaza root-part = ESP" # ── la instalación de verdad ────────────────────────────────────────────────────────────────── # ⚠ La huella de la vecina se toma ACÁ y no antes de las guardas, y la primera corrida de este test # explicó por qué: **montar un ext4 lo MODIFICA** (el superbloque guarda el último montaje y marca # el fs como montado), así que el `mount`/`umount` de la guarda de «partición montada» ya cambia el # sha256 del device. Medirlo antes daba un ROJO que no era del instalador — un test que acusa al # inocente se desactiva a la semana, que es peor que no tenerlo. Lo que este test tiene que probar # es que **la instalación** no la toca, y eso es exactamente lo que mide desde acá. sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes" # ⚠ Se instala por **PARTUUID**, no por `/dev/…`, y el banco lo hace así a propósito: los nombres # `nvmeXn1` NO son estables entre arranques (medido en QEMU con dos NVMe: el disco declarado primero # apareció como el segundo, y al rearrancar se cruzaron otra vez). Un instalador que confía en el # nombre puede formatear el disco equivocado. Si esta parte del test se cambia a un device suelto, # se pierde justamente la cobertura que importa. _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}}') [ -n "$_pu" ] && ok "PARTUUID del destino: $_pu" || falla "no pude leer el PARTUUID de ${LOOP}p2" echo "==> instalando en PARTUUID=$_pu (que hoy es ${LOOP}p2)" ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \ ./scripts/takana-install-partition.sh --root-part "PARTUUID=$_pu" --esp "${LOOP}p1" --no-nvram --yes 2>"$P/stderr" | tail -4 if [ "$ESP_SANA" = "0" ]; then grep -q "declara más espacio del que existe" "$P/stderr" \ && ok "el guardián de capacidad NO se cree al FAT mentiroso" \ || falla "el FAT mentiroso pasó sin advertencia" fi # ── lo que hay que comprobar es lo que NO cambió ────────────────────────────────────────────── echo "==> veredicto" [ "$(sha256sum "${LOOP}p3" | cut -d' ' -f1)" = "$(cat "$P/p3-antes")" ] \ && ok "la partición del usuario quedó idéntica byte a byte" || falla "LA PARTICIÓN DEL USUARIO CAMBIÓ" "$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-ahora" cmp -s "$P/tabla-antes" "$P/tabla-ahora" && ok "la tabla de particiones no se tocó" || falla "LA TABLA CAMBIÓ" mkdir -p "$P/esp"; mount -t vfat "${LOOP}p1" "$P/esp" _a=$(sha256sum "$P/ajeno.bin" | cut -d' ' -f1) [ "$(sha256sum "$P/esp/EFI/BOOT/BOOTX64.EFI" | cut -d' ' -f1)" = "$_a" ] \ && ok "\\EFI\\BOOT\\BOOTX64.EFI del vecino intacto" || falla "SE PISÓ EL FALLBACK DEL VECINO" [ "$(sha256sum "$P/esp/EFI/Microsoft/Boot/bootmgfw.efi" | cut -d' ' -f1)" = "$_a" ] \ && ok "\\EFI\\Microsoft\\ intacto" || falla "SE TOCÓ EL WINDOWS DEL VECINO" [ "$(sha256sum "$P/esp/EFI/takana/takanax64.efi" | cut -d' ' -f1)" = "$(sha256sum "$KERNEL" | cut -d' ' -f1)" ] \ && ok "nuestro kernel llegó entero a \\EFI\\takana\\" || falla "el kernel de la ESP no coincide con el bzImage" [ -r "$P/esp/initramfs.cpio.gz" ] && ok "el initramfs de pivote está en la raíz de la ESP" || falla "falta \\initramfs.cpio.gz" umount "$P/esp" _lbl=$(blkid -s LABEL -o value "${LOOP}p2" 2>/dev/null || true) [ "$_lbl" = "hammer-root" ] && ok "el destino quedó ext4 LABEL=hammer-root" || falla "LABEL del destino = '${_lbl:-}' (esperaba hammer-root)" mkdir -p "$P/dst"; mount "${LOOP}p2" "$P/dst" [ -x "$P/dst/sbin/init" ] && ok "/sbin/init instalado (el wrapper que reconcilia el arranque)" || falla "el destino no tiene /sbin/init" umount "$P/dst" # Y el contenido del vecino, no sólo su hash: un fichero que sigue ahí y se lee es la prueba que el # usuario entiende. mount "${LOOP}p3" "$P/mnt" [ "$(cat "$P/mnt/NO-BORRAR.txt" 2>/dev/null)" = "la tesis del usuario, 2026" ] \ && ok "el fichero del usuario se sigue leyendo en la partición vecina" || falla "SE PERDIÓ EL FICHERO DEL USUARIO" umount "$P/mnt" # ── escenario 2: instalar AL LADO, en el hueco sin asignar ──────────────────────────────────── # Es el camino al que cae la política de la raíz viva cuando la máquina no puede kexec, así que # tiene que estar igual de probado. Lo que se comprueba es que la tabla crece en UNA entrada y que # las que ya estaban no se mueven ni un sector. echo echo "==> escenario 2: --use-free-space (instalar al lado, en el hueco)" "$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-antes2" _n_antes=$(wc -l < "$P/tabla-antes2") sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes2" ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \ ./scripts/takana-install-partition.sh --use-free-space --disk "$LOOP" --esp "${LOOP}p1" \ --no-nvram --yes 2>"$P/stderr2" | tail -3 "$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-ahora2" _n_ahora=$(wc -l < "$P/tabla-ahora2") [ "$_n_ahora" = "$(( _n_antes + 1 ))" ] && ok "la tabla creció en UNA entrada ($_n_antes → $_n_ahora)" \ || falla "la tabla pasó de $_n_antes a $_n_ahora entradas" head -n "$_n_antes" "$P/tabla-ahora2" > "$P/tabla-viejas2" cmp -s "$P/tabla-antes2" "$P/tabla-viejas2" && ok "las particiones que ya estaban no se movieron" \ || falla "SE MODIFICÓ ALGUNA ENTRADA EXISTENTE" [ "$(sha256sum "${LOOP}p3" | cut -d' ' -f1)" = "$(cat "$P/p3-antes2")" ] \ && ok "los datos del usuario siguen idénticos tras instalar al lado" || falla "LA VECINA CAMBIÓ AL INSTALAR AL LADO" _nueva=$(tail -1 "$P/tabla-ahora2" | cut -d' ' -f1 | tr -d ':') [ "$(blkid -s LABEL -o value "$_nueva" 2>/dev/null)" = "hammer-root" ] \ && ok "la partición nueva ($_nueva) quedó ext4 LABEL=hammer-root" || falla "la partición nueva no tiene el filesystem esperado" # ── escenario 3: el MODO INTERACTIVO, bajo un pty ───────────────────────────────────────────── # El menú sólo aparece si el instalador ve una terminal de verdad (`[ -t 0 ]`), así que no se puede # probar con un simple pipe: haría falta una tty. Se le da una con `python3 -m pty`, que es la forma # de automatizar exactamente el camino que recorre una persona. Se comprueba (1) que el censo # clasifica bien —que lo cifrado y lo de Windows NO se ofrecen— y (2) que la elección se convierte # en un PARTUUID y no en un nombre de device. echo echo "==> escenario 3: modo interactivo (censo + elección) bajo pty" _n_destino=$("$SFDISK" -d "$LOOP" 2>/dev/null | awk '/^\/dev/{n++} END{print n}') _menu_out="$P/menu.txt" ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" TAKANA_TEST_MENU=1 \ python3 - "$PWD" "$LOOP" > "$_menu_out" 2>&1 <<'PYEOF' || true import os, pty, sys, time cwd, loop = sys.argv[1], sys.argv[2] # Se responde "no": lo que se valida es el CENSO y las opciones ofrecidas, no otra instalación. salida = [] def leer(fd): d = os.read(fd, 1024) salida.append(d) return d pid, fd = pty.fork() if pid == 0: os.chdir(cwd) os.execv("/bin/sh", ["sh", "scripts/takana-install-partition.sh"]) buf = b"" t0 = time.time() while time.time() - t0 < 120: try: d = leer(fd) except OSError: break if not d: break buf += d if b"salir sin tocar nada" in buf: os.write(fd, b"no\n") break try: while True: d = leer(fd) if not d: break except OSError: pass os.waitpid(pid, 0) sys.stdout.write(b"".join(salida).decode("utf-8", "replace")) PYEOF grep -q "el terreno de esta" "$_menu_out" && ok "el menú muestra el censo del terreno" || falla "el modo interactivo no mostró el censo" grep -q "opciones:" "$_menu_out" && ok "el menú ofrece opciones numeradas" || falla "el menú no ofreció opciones" grep -qi "NO TOCAR" "$_menu_out" && ok "hay particiones marcadas NO TOCAR" || falla "el censo no marcó ninguna partición como intocable" grep -q "PARTUUID=" "$_menu_out" && ok "el censo publica el PARTUUID de cada partición" || falla "el censo no muestra PARTUUID" grep -q "cancelado: no se tocó nada" "$_menu_out" && ok "responder «no» sale sin tocar nada" || falla "la salida por «no» no se comportó" echo [ "$FALLOS" = "0" ] && { echo "VERDE: instaló sin tocar nada de lo ajeno ($LOOP)"; exit 0; } echo "ROJO: $FALLOS comprobación(es) fallaron" >&2; exit 1