Files
takana/docs/13-release-engineering.md
T
sergioandClaude Opus 4.8 d23946fa3d hammer-upgrade: upgrades atómicos con generaciones y rollback (Etapa E4, primer corte)
Crate hammer-upgrade + CLI 'hammer upgrade apply|rollback|status'. Aplica un
árbol Stage1/producto del store sobre el FHS vivo dejando una generación
(manifest + backup de bytes previos), atómico fichero-a-fichero con 'current'
como commit-point. Rollback restaura EXACTO al árbol anterior (o pre-upgrades).
Cada cambio al journal (HammerHydrate, replay-able). Verificación opcional del
of_tree esperado (índice de mirror E3 / release firmada). Maneja ficheros +
symlinks. 7 tests unitarios + scripts/upgrade-e2e-test.sh (apply v1->v2->
rollback->rollback contra el binario real). SDD 13 actualizado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:46:14 -04:00

87 lines
6.8 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). **Endurecimiento pendiente:** journal de
*intención* + replay para sobrevivir un kernel-panic a media escritura; GC de generaciones viejas
(prune); cableado del init de boot para que el rollback sea seleccionable al arranque.
- **E5 — ISO/medio de arranque:** medio live para correr el instalador (xorriso/grub-mkrescue;
herramental a construir).