Files
takana/docs/13-release-engineering.md
T
sergioandClaude Opus 4.8 d73e20a394 instalar DESDE el live: hammer-install in-live + payload de disco (refinamiento #1)
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>
2026-06-21 01:50:07 -04:00

125 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 AD 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.**