Files
hammer/docs/runbooks/stage1-vm-boot.md
T
SergioandClaude Opus 4.8 56af1b5748 docs: runbook para validar Stage 1 booteando en QEMU
Procedimiento operativo end-to-end para de-riesgar lo construido: stage0 (ingerir
zig) → stage1 (build + ensamblar el rootfs) → empaquetar initramfs (cpio newc +
gzip) → bootear en QEMU con el kernel del host → criterios de éxito.

Fundamentado en los comandos reales del repo (scripts/bootstrap-devfs.sh, la URL
y sha256 de zig 0.16.0, hammer bootstrap stage0/stage1). La prueba clave: matar
hammerd y ver a arje-zero reiniciarlo con backoff — el CRASHED real.

Honesto sobre su estado: es el procedimiento, no un transcript verificado; el
cross-compile y el boot reales aún no se corrieron (ese es el punto). §7
anticipa los ajustes probables (init, /dev/console, getty/tty, -lgcc_s).

Enlazado desde SDD 12 §I2 y el índice de docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:00:29 +00:00

6.8 KiB
Raw Blame History

Runbook — validar Stage 1 booteando en QEMU

Operativo, no diseño. Valida end-to-end que el rootfs que produce hammer bootstrap stage1 (SDD 11) bootea con arje-zero como PID 1 (SDD 12), levanta hammerd + getty bajo supervisión, y que el CRASHED real funciona.

Esto es un procedimiento, no un transcript verificado. La lógica de ensamblado/hash/seed está cubierta por unit tests (y la seed se validó contra card_core::Card), pero el cross-compile y el boot reales no se han corrido todavía — ese es justo el punto de este runbook. La primera corrida puede destapar ajustes (link, /dev/console, getty/tty); §7 los anticipa.

0. Lo que valida / lo que no

  • Valida: que el árbol sellado por stage1 arranca como sistema: arje-zero monta los pseudo-FS, carga la seed, incarna hammerd y la getty, y al matar hammerd lo reinicia con backoff (el CRASHED que la Fase 5 difirió).
  • No valida (aún): bus único (B.2), overlay del store (I4), atestación (A1/A2), kernel from-source. Aquí el kernel es prestado del host; el overlay no se monta.

1. Prerrequisitos

  • Lab de dev montado: ./scripts/bootstrap-devfs.sh (deja .dev-fs/alpine + .dev-fs/tools/zig).
  • Red: el build de hammerd y arje-zero clona repos git (hammer, tawasuyu) y corre cargo vendor en el fetch. El build hermético del sandbox es --offline, pero el fetch necesita red.
  • Herramientas host: qemu-system-x86_64, un kernel x86_64 (/boot/vmlinuz-*), cpio, gzip.
  • Espacio/tiempo: construir arje-zero vendorea las deps del monorepo tawasuyu entero — es pesado y lento la primera vez (se cachea después).
  • El binario hammer: cargo build --release -p hammer-cli./target/release/hammer.

El store debe vivir en la raíz del repo (./store) para que BuildConfig resuelva .dev-fs/alpine como hermano (store/.. = repo root). El toolchain de Stage 1 no es el de .dev-fs: es la semilla de Stage 0 (paso 2), que stage1 enchufa como zig_dir del sandbox.

2. Stage 0 — ingerir la semilla (zig)

export HAMMER=./target/release/hammer
export STORE=./store

"$HAMMER" --store "$STORE" bootstrap stage0 \
  --url https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz \
  --sha256 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 \
  --version 0.16.0
# → imprime el seed_hash (b3:...). Guardalo:
SEED="b3:..."   # pegá el hash impreso

Idempotente: re-correr no re-descarga. La semilla queda sellada como seed-zig en el store y anota su línea en bootstrap.json.

3. Stage 1 — construir y sellar el rootfs

"$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED" --recipes recipes
# construye musl + busybox + hammerd + arje-zero, ensambla el rootfs FHS y lo sella.
# → imprime el rootfs_hash (b3:...).

# El árbol sellado (read-only) del rootfs:
ROOTFS_DIR=$(ls -d "$STORE"/*-stage1-rootfs)
echo "$ROOTFS_DIR"
ls "$ROOTFS_DIR"/ente/seed.card.json "$ROOTFS_DIR"/sbin/init "$ROOTFS_DIR"/usr/bin/{hammerd,arje-zero}

Necesita red (git clone + cargo vendor). Si un build Cargo falla en el link (-lgcc_s) o por static-PIE, ver §7 — el zig cc del lab debería resolver -lgcc_s (bitácora M2 del plan arje↔hammer).

4. Empaquetar el rootfs como initramfs

El árbol del store ya trae /sbin/init/usr/bin/arje-zero y /ente/seed.card.json. Se empaqueta como cpio newc + gzip (formato initramfs):

( cd "$ROOTFS_DIR" && find . -print0 | cpio --null -o --format=newc ) | gzip -9 > /tmp/stage1.cpio.gz
ls -lh /tmp/stage1.cpio.gz

cpio newc preserva symlinks y hardlinks. El symlink /sbin/init/usr/bin/arje-zero es absoluto: en un initramfs resuelve dentro de su propia raíz. (Si el kernel no auto-monta devtmpfs, ver §7 por el nodo /dev/console.)

5. Bootear en QEMU

qemu-system-x86_64 -m 512 -nographic \
  -kernel /boot/vmlinuz-linux \
  -initrd /tmp/stage1.cpio.gz \
  -append "console=ttyS0 rdinit=/sbin/init"
  • rdinit=/sbin/init — el default de un initramfs es /init; lo sobreescribimos a /sbin/init (→arje-zero). Alternativa directa: rdinit=/usr/bin/arje-zero.
  • console=ttyS0 + -nographic — consola serie, donde arje escribe y la getty se engancha.
  • Salir de QEMU: Ctrl-A luego X.

6. Criterios de éxito (qué observar)

  1. PID 1: en kmsg/consola, arje-zero loguea "despierta como PID 1".
  2. Pseudo-FS: monta /proc /sys /dev sin error fatal (best-effort; un EBUSY se ignora).
  3. Seed: carga /ente/seed.card.json sin "card inválida" (ya validada contra card_core::Card).
  4. hammerd: arranca como Ente — su tracing aparece y/o crea /run/agent.sock.
  5. getty: aparece un prompt de shell (/bin/sh) en ttyS0.
  6. CRASHED real — la prueba clave. En el shell:
    kill "$(pidof hammerd)"
    
    arje registra el on_death(ExitStatus) y reinicia hammerd tras el backoff (200 ms → ×2…). Eso es el CRASHED que la Fase 5 dejó diferido, ahora manejado por el init propio.

7. Troubleshooting (síntoma → causa → fix)

Síntoma Causa probable Fix
Kernel panic … no init found /sbin/init no ejecuta probar rdinit=/usr/bin/arje-zero; verificar que el symlink quedó en el cpio (cpio -tv)
arje cae a rescue shell "card inválida" la seed no validó confirmar que /ente/seed.card.json se empaquetó; comparar con el schema de seeds/arje-qemu.card.json
hammerd/arje-zero: exec format error binario no estático / falta loader confirmar link = "static"; un static-PIE pide /lib/ld-musl-x86_64.so.1 — para init-clásico desactivar PIE (-C relocation-model=static, bitácora M2)
Build Cargo falla en -lgcc_s hueco del toolchain musl dinámico el lab usa zig cc (empaqueta su compiler-rt) → no debería aparecer; verificar compiler = "zig-cc" en la receta
getty sin prompt device console no engancha tty probar argv con ttyS0 en vez de console; revisar que /dev/console existe
No working init / sin /dev/console kernel sin auto-mount de devtmpfs crear el nodo en el cpio (mknod -m 600 dev/console c 5 1 antes de empaquetar) o usar un kernel con CONFIG_DEVTMPFS_MOUNT=y
cargo vendor / git clone falla el fetch necesita red correr Stage 1 con red; el sandbox de build sigue siendo --offline

8. Cross-check opcional — arje-packager

arje trae su propio empaquetador (03_ukupacha/arje/init/arje-packager): arje-packager --seed <CARD.json> --out <initramfs.cpio.gz> [--bin LABEL=PATH]…. Útil para comparar el initramfs canónico de arje contra el que sale del rootfs de hammer (§4). No reemplaza esta validación: aquí probamos el rootfs que hammer sella, no el que arje arma.