diff --git a/docs/13-release-engineering.md b/docs/13-release-engineering.md new file mode 100644 index 00000000..1f3e44fa --- /dev/null +++ b/docs/13-release-engineering.md @@ -0,0 +1,60 @@ +# SDD 13 — Release engineering (Etapa E): imagen auto-booteable, instalador, mirror, upgrades + +> Estado: **EN CURSO**. Etapa E del roadmap (`docs/10-roadmap.md`, memoria distro-completion-roadmap). +> Es trabajo nuevo: las Etapas A–D dejaron un sistema que (A) se orquesta, (B) bootea de disco +> particionado real, (C) tiene userland Rust-nativo y (D) atesta confianza. Falta convertir eso en algo +> que un tercero **instale y arranque sin el host de dev**: una imagen que bootea sola (sin +> `qemu -kernel`), un instalador a disco físico, un mirror del store y un camino de upgrade. + +## 1. El gap concreto que abre la Etapa E + +Hasta B3 la imagen de disco (`scripts/disk-image.sh`, GPT con `/`, `/store`, `/var/lib/hammer` +dedicados) **no se auto-bootea**: el `drive-rebuild.py` la arranca con `qemu -kernel +-append root=/dev/vdaN` — el kernel lo carga QEMU, no la imagen. Eso sirve para verificar el +auto-alojamiento, pero un disco así no arranca en hardware real ni en una VM "normal": le falta un +**bootloader** en el propio disco. + +## 2. Decisión de bootloader: GRUB BIOS (i386-pc) ahora, UEFI después + +- **GRUB BIOS (i386-pc) — ELEGIDO para el primer corte.** QEMU trae SeaBIOS built-in ⇒ un disco con + GRUB en el MBR + una *BIOS boot partition* (GPT type `ef02`) bootea con `qemu-system-x86_64 -drive + file=img` **sin firmware extra ni `-kernel`**. Se instala **en userspace, sin root ni loop**: + `grub-mkimage` arma `core.img` y `grub-bios-setup` escribe `boot.img`→MBR + `core.img`→partición + BIOS sobre el *fichero* imagen (es I/O de bloques, no necesita montar). Los módulos de GRUB y el + kernel van en `/boot` de la partición root, que `mke2fs -d` puebla desde un directorio. +- **UEFI (EFI-stub + ESP FAT) — diferido.** El kernel ya trae `CONFIG_EFI_STUB=y`, pero (a) el host de + dev no tiene firmware OVMF usable para verificar el arranque, y (b) poblar un ESP FAT sin root exige + `mtools` (ausente). Camino futuro cuando haya OVMF + mtools (o se construyan): ESP con + `EFI/BOOT/BOOTX64.EFI` = el bzImage (EFI-stub), cmdline por `loader.conf`/UKI. + +## 3. Layout de la imagen instalable (GPT, BIOS) + +``` +/dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB +/dev/vda2 / ext4 [hammer-root] + /boot/bzImage + /boot/grub +/dev/vda3 /store ext4 [hammer-store] artefactos CAS (inmutable) +/dev/vda4 /var/lib/hammer ext4 [hammer-state] estado mutable (journal, overlays) +``` + +Cadena de arranque: SeaBIOS → MBR (boot.img) → core.img (BIOS boot part) → lee `(hd0,gpt2)/boot/grub/ +grub.cfg` → `linux /boot/bzImage root=/dev/vda2 rdinit=/sbin/init` → wrapper `/sbin/init` monta +vda3/vda4 → `exec /usr/bin/arje-zero` (PID 1). El wrapper es el mismo puente de B3 (arje todavía no lee +fstab/cards de montaje por sí mismo). + +`scripts/install-image.sh` produce esta imagen. Reusa la técnica de particionado sin-root de B3 +(sfdisk + `mke2fs -d` bajo `unshare -r` + `dd conv=sparse,notrunc`) y añade la BIOS boot partition, el +kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-mkimage`/`grub-bios-setup`. + +## 4. Piezas pendientes de la Etapa E (orden tentativo) + +- **E1 — imagen auto-booteable (GRUB BIOS): ✅ primer corte** (`scripts/install-image.sh`). Bootea en + QEMU sin `-kernel` hasta la shell de arje-zero. Hoy empaqueta el *builder rootfs* (con toolchain); + un *product rootfs* lean es refinamiento. +- **E2 — instalador a disco físico:** un `hammer install ` que particiona un disco real y + vuelca las tres particiones + GRUB (la misma lógica, apuntando a `/dev/sdX` en vez de un fichero). +- **E3 — mirror del store:** servir/replicar `/store` content-addressed (BLAKE3) entre máquinas; el + hash ES la dirección, así que un mirror es un CAS replicado + resolución por hash. +- **E4 — upgrades:** aplicar un nuevo árbol Stage 1 sin reinstalar — atómico vía el modelo overlay + + el journal (D), con rollback al árbol anterior. +- **E5 — ISO/medio de arranque:** medio live para correr el instalador (xorriso/grub-mkrescue; + herramental a construir). diff --git a/scripts/install-image.sh b/scripts/install-image.sh new file mode 100644 index 00000000..a29f276a --- /dev/null +++ b/scripts/install-image.sh @@ -0,0 +1,171 @@ +#!/bin/sh +# install-image.sh — Etapa E1 (SDD 13): produce una imagen de disco AUTO-BOOTEABLE. A diferencia de +# scripts/disk-image.sh (B3, que se arranca con `qemu -kernel` para verificar el auto-alojamiento), +# ésta lleva GRUB en el propio disco ⇒ bootea con `qemu-system-x86_64 -drive file=img` SIN `-kernel` ni +# firmware extra (SeaBIOS de QEMU → MBR → core.img → kernel), como en hardware real. +# +# Layout (GPT, BIOS): +# /dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB +# /dev/vda2 / ext4 [hammer-root] + /boot/bzImage + /boot/grub +# /dev/vda3 /store ext4 [hammer-store] +# /dev/vda4 /var/lib/hammer ext4 [hammer-state] +# +# Instalación de GRUB en USERSPACE (sin root ni loop): grub-mkimage arma core.img; grub-bios-setup +# escribe boot.img→MBR + core.img→partición BIOS sobre el fichero imagen (I/O de bloques). Los módulos +# de GRUB + el kernel van en /boot de la root, que mke2fs -d puebla desde un directorio. +# +# Uso: +# ./scripts/install-image.sh # crea work/hammer-install.img +# BOOT=1 KVM=1 ./scripts/install-image.sh # además lo arranca (auto-boot, sin -kernel) +# +# Variables: +# ROOTFS dir rootfs a empaquetar (default work/builder-rootfs) +# KERNEL bzImage a embeber en /boot (default el del store: store/*-linux/boot/bzImage) +# IMG imagen de salida (default work/hammer-install.img) +# ROOT_SIZE MiB de / (default 4096) +# STORE_SIZE MiB de /store (default 2048) +# STATE_SIZE MiB de /var/lib/hammer (default 2048) +# CMDLINE línea de comando del kernel (default "console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init") +# BOOT 1 ⇒ arranca la imagen con QEMU tras crearla (auto-boot, sin -kernel) +# KVM/MEM passthrough al arranque de prueba +set -eu +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +ROOTFS="${ROOTFS:-work/builder-rootfs}" +IMG="${IMG:-work/hammer-install.img}" +ROOT_SIZE="${ROOT_SIZE:-4096}" +STORE_SIZE="${STORE_SIZE:-2048}" +STATE_SIZE="${STATE_SIZE:-2048}" +CMDLINE="${CMDLINE:-console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init}" +KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}" +GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}" + +[ -d "$ROOTFS" ] || { echo "no existe ROOTFS: $ROOTFS (corré selfhost-verify para armar el builder)" >&2; exit 1; } +[ -e "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&2; exit 1; } +[ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel a embeber (set KERNEL=)" >&2; exit 1; } +[ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/boot.img" ] || { echo "faltan los módulos GRUB i386-pc en $GRUB_LIB" >&2; exit 1; } +for t in sfdisk mke2fs grub-mkimage grub-bios-setup; do + command -v "$t" >/dev/null 2>&1 || { echo "falta $t" >&2; exit 1; } +done +unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; } + +STAGE="work/.install-stage" +rm -rf "$STAGE" +mkdir -p "$STAGE" +ROOT_TREE="$STAGE/root" +ROOT_IMG="$STAGE/root.img" +STORE_IMG="$STAGE/store.img" +STATE_IMG="$STAGE/state.img" +GRUB_STAGE="$STAGE/grub" + +# --- árbol root: hardlinks del rootfs SIN store/estado + /boot (kernel + GRUB) + wrapper /sbin/init --- +echo "==> staging root: rootfs (hardlinks) + /boot/bzImage + /boot/grub + wrapper /sbin/init" +cp -al "$ROOTFS" "$ROOT_TREE" +rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" +mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" "$ROOT_TREE/boot/grub/i386-pc" +cp "$KERNEL" "$ROOT_TREE/boot/bzImage"; chmod 0644 "$ROOT_TREE/boot/bzImage" +# Módulos GRUB que core.img cargará en tiempo de boot desde /boot/grub/i386-pc. +cp "$GRUB_LIB"/*.mod "$GRUB_LIB"/*.lst "$ROOT_TREE/boot/grub/i386-pc/" 2>/dev/null || true +# grub.cfg con consola serie (para -nographic) y el menuentry del kernel embebido. +cat > "$ROOT_TREE/boot/grub/grub.cfg" < "$ROOT_TREE/sbin/init" <<'INIT' +#!/bin/sh +# Wrapper PID1 (Etapa E/B3): el kernel monta vda2 como /; montamos las particiones dedicadas. +/bin/busybox mount -t ext4 /dev/vda3 /store || echo "init: no pude montar /store (/dev/vda3)" +/bin/busybox mount -t ext4 /dev/vda4 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda4)" +exec /usr/bin/arje-zero +INIT +chmod +x "$ROOT_TREE/sbin/init" + +# --- GPT: vda1=BIOS boot (ef02), vda2=/, vda3=/store, vda4=/var/lib/hammer --- +SECT_MIB=2048 +BIOS_SECT=4096 # 2 MiB para core.img +ROOT_SECT=$(( ROOT_SIZE * SECT_MIB )) +STORE_SECT=$(( STORE_SIZE * SECT_MIB )) +STATE_SECT=$(( STATE_SIZE * SECT_MIB )) +START1=2048 +START2=$(( START1 + BIOS_SECT )) +START3=$(( START2 + ROOT_SECT )) +START4=$(( START3 + STORE_SECT )) +TOTAL_SECT=$(( START4 + STATE_SECT + 2048 )) +BIOS_GUID="21686148-6449-6E6F-744E-656564454649" # GPT "BIOS boot partition" + +echo "==> imagen : $IMG (GPT BIOS-boot, root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)" +rm -f "$IMG" +truncate -s $(( TOTAL_SECT * 512 )) "$IMG" +sfdisk --quiet "$IMG" >/dev/null < mke2fs + empalme de las particiones ext4 (unshare -r, sin sudo)" +unshare -r sh -eu < instalar GRUB (grub-mkimage + patch binario boot/core, sin root/loop)" +mkdir -p "$GRUB_STAGE" +# core.img: prefix a (hd0,gpt2)/boot/grub (root = 2ª partición GPT); embebemos lo justo para leer +# GPT+ext2, cargar grub.cfg (normal/configfile) y el kernel (linux). +grub-mkimage -O i386-pc -p '(hd0,gpt2)/boot/grub' -o "$GRUB_STAGE/core.img" \ + biosdisk part_gpt ext2 normal configfile linux echo ls boot +# Punteros (ABI estable de GRUB i386-pc): +# boot.img off 0x5c kernel_sector (u64 LE): LBA del 1er sector de core.img → START1 +# core.img off 0x1F4 blocklist.start (u64 LE): LBA del RESTO de core.img → START1+1 +# Verificamos los valores por defecto (1 y 2: core.img asumido contiguo tras el MBR) antes de tocar. +START1="$START1" GRUB_LIB="$GRUB_LIB" GRUB_STAGE="$GRUB_STAGE" IMG="$IMG" python3 - <<'PY' +import os, struct +s1 = int(os.environ["START1"]) +img = os.environ["IMG"] +boot = open(os.path.join(os.environ["GRUB_LIB"], "boot.img"), "rb").read() +core = bytearray(open(os.path.join(os.environ["GRUB_STAGE"], "core.img"), "rb").read()) +boot = bytearray(boot) +assert struct.unpack_from(" imagen instalable creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales" +echo " auto-boot: qemu-system-x86_64 -drive file=$IMG,format=raw -nographic (sin -kernel)" + +if [ "${BOOT:-0}" = 1 ]; then + echo "==> arranque de prueba (auto-boot, sin -kernel)" + accel="" + [ "${KVM:-0}" = 1 ] && [ -w /dev/kvm ] && accel="-enable-kvm -cpu host" + # shellcheck disable=SC2086 + exec qemu-system-x86_64 -m "${MEM:-4096}" -no-reboot -nographic $accel \ + -drive file="$IMG",format=raw,if=virtio +fi