Cierra el lazo ISO live -> disco instalado -> bootea solo. iso-image.sh INSTALLER=1 bundlea kernel + GRUB MBR (boot/core.img + modulos) en el initramfs bajo /usr/lib/hammer/install/ + inyecta /usr/bin/hammer-install. hammer-install corre dentro del live como root real (busybox fdisk/mke2fs/mount/dd + uutils cp): particiona MBR (/,/store,/var/lib/hammer), formatea, copia la propia raiz del live (autoinstalador), escribe /boot+wrapper, instala GRUB con 2 dd (boot->MBR, core->hueco post-MBR; punteros default 1/2 ya valen en MBR contiguo, sin parcheo). AUTO_INSTALL=<dev> = /init desatendido (install+poweroff). iso-install-test.sh valida E2E: ISO+disco blanco -> HAMMER-INSTALL-OK -> boot del disco solo -> GRUB->kernel->arje-zero->sshd, particiones dedicadas montadas. GOTCHAS: busybox fdisk CHS-alinea a 63 (hueco 62 < core 281 sectores) -> pisa el FS de p1 -> grub>; fix sectores explicitos (p1@2048). Copiar top-level de / enumerado, no lista fija, o se escapa /ente/seed.card.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
11 KiB
Markdown
125 lines
11 KiB
Markdown
# 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 <bzImage>
|
||
-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 <device>` 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: ✅ primer corte** (crate `hammer-mirror` + CLI `hammer mirror push|pull|status`).
|
||
Replica `/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. La integridad se ancla en `of_tree` (content-hash del árbol): el
|
||
receptor **recomputa** `of_tree` sobre lo recibido y exige que case el del índice **antes** de sellar
|
||
(`Store::seal` atómico + read-only) — una transferencia corrupta/manipulada se **rechaza**, no se instala.
|
||
`push`/`pull` son `sync(src,dst)` (idempotente, salta presentes, reporta conflictos = mismo hash con otro
|
||
contenido). Transporte de esta entrega = **sistema de ficheros** (el remoto es una ruta a otro store, p.ej.
|
||
un montaje sshfs); el transporte sobre SSH/red que el producto ya expone es un envoltorio posterior.
|
||
Endurecimiento pendiente: anclar el `of_tree` a una raíz firmada (log de transparencia `bootstrap.json` /
|
||
atestación de Etapa D) para defenderse de un origen plenamente malicioso (hoy el invariante forzado es
|
||
"el contenido recibido hashea a lo que el índice anuncia", que ataja corrupción de transporte).
|
||
- **E4 — upgrades: ✅ primer corte** (crate `hammer-upgrade` + CLI `hammer upgrade apply|rollback|status`).
|
||
Aplica un árbol Stage1/producto **nuevo** (artefacto sellado del store, CAS BLAKE3) sobre el FHS vivo
|
||
sin reinstalar, dejando una **generación**, y revierte exactamente al árbol anterior con `rollback`.
|
||
**Modelo de generaciones** (inspirado en NixOS, in-place al FHS porque el boot lee el root directo, no
|
||
un symlink indirecto): cada generación graba bajo `<state>/generations/<N>/` su `manifest.json`
|
||
(tree_dir del store, `of_tree`, parent, lista de ficheros, cambios) y un `backup/` con los bytes
|
||
**previos** de todo path que pisó o retiró. **Atomicidad:** proyección fichero-a-fichero (escritura a
|
||
temporal + `rename` ⇒ cada fichero conmuta atómicamente), con `current` (id de la generación viva) como
|
||
commit-point; un corte a mitad deja `current` en la vieja y los backups intactos ⇒ re-apply/rollback
|
||
recuperan. **Diff vs el árbol previo:** el apply añade/pisa los ficheros del árbol nuevo y **retira** los
|
||
que la generación viva aportaba y el nuevo no tiene. **Rollback:** deshace en orden inverso (borra lo
|
||
añadido, restaura desde `backup/` lo pisado/retirado) y retrocede `current` al padre — restauración
|
||
*exacta*, incluso al estado pre-upgrades. Cada cambio se registra en el [journal](05-journal.md) como
|
||
`HammerHydrate{artifact=tree_dir}` (replay-able). Verificación opcional de integridad: si el llamador
|
||
anuncia el `of_tree` esperado (de un índice de **mirror E3** o release firmada), el apply lo exige antes
|
||
de tocar el root. Maneja ficheros regulares + symlinks; valida E2E en host con `scripts/upgrade-e2e-test.sh`
|
||
(apply v1→v2→rollback→rollback contra el binario real). **GC de generaciones ✅** (`hammer upgrade
|
||
prune [--keep N]` / `hammer_upgrade::prune`): borra las generaciones que el rollback ya no alcanza
|
||
(huérfanas tras un rollback — las de la [cadena viva](#) siempre se conservan); `--keep N` además
|
||
recorta la cadena a sus N más nuevas (limita la profundidad de rollback, como `delete-generations` en
|
||
NixOS). **Endurecimiento pendiente:** journal de *intención* + replay para sobrevivir un kernel-panic a
|
||
media escritura; cableado del init de boot para que el rollback sea seleccionable al arranque.
|
||
- **E5 — ISO/medio de arranque: ✅ primer corte** (`recipes/xorriso.toml` + `scripts/iso-image.sh` +
|
||
`scripts/iso-boot-test.sh`). Medio **live** que arranca ENTERO desde RAM: GRUB (El Torito) carga
|
||
kernel + initramfs desde el ISO 9660, el kernel desempaqueta el rootfs del producto a un tmpfs y corre
|
||
`/init` — sin tocar disco. Es el medio para correr el instalador (`hammer install /dev/sdX`) en
|
||
hardware nuevo o probar el sistema sin instalarlo. **Herramental construido por hammer:** el host no
|
||
trae `xorriso` (y `grub-mkrescue` lo exige) ⇒ `recipes/xorriso.toml` lo construye desde fuente (GNU
|
||
xorriso 1.5.8 = libburn+libisofs+libisoburn, zig 0.13.0, estático musl, libs opcionales off ⇒ binario
|
||
autocontenido). `iso-image.sh` lo **dogfooda**: arma el ISO con el xorriso del store. **Cadena de
|
||
boot BIOS:** reusa los módulos GRUB i386-pc de E1 pero con el core El Torito (`grub-mkimage -O
|
||
i386-pc-eltorito` + módulos `iso9660`/`biosdisk`) en vez del core de disco (ext2); `xorriso -as
|
||
mkisofs -b …/eltorito.img -no-emul-boot -boot-info-table --grub2-boot-info --grub2-mbr boot_hybrid.img`
|
||
⇒ ISO **isohybrid** (booteable como CD *y* USB/disco). El initramfs es el `product-rootfs` lean
|
||
empaquetado como `cpio.gz` (live de RAM; la variante con squashfs+overlay para rootfs grandes es
|
||
refinamiento). Validado E2E in-VM (`iso-boot-test.sh`, ISO 113M): SeaBIOS → GRUB 2.14 (El Torito) →
|
||
Linux 6.16.12 hammer-built → `/init` (marcador `HAMMER-ISO-LIVE-OK`) → arje-zero PID1 → hammerd +
|
||
netup (lease DHCP) → **sshd escuchando**: el producto entero corriendo desde el medio live. **Pendiente
|
||
(refinamiento):** squashfs+overlay para no cargar todo el rootfs en RAM; build EFI (el kernel ya trae
|
||
EFI_STUB, falta poblar la ESP).
|
||
- **Instalar DESDE el live ✅** (`scripts/iso-image.sh INSTALLER=1` + `scripts/hammer-live-install.sh`
|
||
→ `/usr/bin/hammer-install` + `scripts/iso-install-test.sh`). Cierra el lazo **ISO live → disco
|
||
instalado → bootea solo**. El ISO instalador bundlea un *payload de disco* en el initramfs bajo
|
||
`/usr/lib/hammer/install/` (kernel + GRUB `boot.img`/`core.img` MBR-prefix `(hd0,msdos1)/boot/grub`
|
||
+ módulos, armado en build-time con `grub-mkimage`). `hammer-install <dev>` corre **dentro del live
|
||
como root real** (sin los rodeos rootless de E1) usando sólo lo que el producto ya trae (busybox
|
||
`fdisk`/`mke2fs`/`mount`/`dd` + uutils `cp`): particiona **MBR** (p1=/, p2=/store, p3=/var/lib/hammer),
|
||
formatea ext2 (montable ext4), copia la **propia raíz del live** (autoinstalador) excluyendo
|
||
virtuales/payload, escribe `/boot` + wrapper `/sbin/init`, e instala GRUB con dos `dd` (boot.img→MBR
|
||
+ core.img→hueco post-MBR). **GOTCHAS:** (a) busybox `fdisk` alinea a CHS (sector 63 ⇒ hueco de 62
|
||
sectores) y el core.img de GRUB (~281 sectores) lo DESBORDA pisando el FS de p1 ⇒ GRUB cae a `grub>`;
|
||
fix = **sectores de inicio/fin EXPLÍCITOS** (p1@2048, hueco de 2047) porque el default de busybox
|
||
ofrece el sector libre más bajo (mete p2 en el hueco pre-p1). (b) Con install contiguo MBR los
|
||
punteros por defecto de GRUB (kernel_sector=1, blocklist=2) **ya valen** ⇒ NO hace falta el parcheo
|
||
binario del caso GPT de E1. (c) copiar el top-level de `/` ENUMERÁNDOLO (no una lista fija) o se
|
||
escapa `/ente/seed.card.json` ⇒ arje-zero aborta "seed.card no encontrada". `AUTO_INSTALL=<dev>`
|
||
hace el `/init` desatendido (install + poweroff) = instalación unattended + scaffolding del test.
|
||
Validado E2E in-VM: boot ISO+disco en blanco → HAMMER-INSTALL-OK → boot del disco solo → GRUB→kernel
|
||
→arje-zero→sshd, /store y /var/lib/hammer montadas. **Refinamiento #1 CERRADO.**
|