From d73e20a394b37b0ae228656529fd1b0ee7b95bd7 Mon Sep 17 00:00:00 2001 From: sergio Date: Sun, 21 Jun 2026 01:50:07 -0400 Subject: [PATCH] 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= = /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 --- docs/13-release-engineering.md | 23 +++++- scripts/hammer-live-install.sh | 144 +++++++++++++++++++++++++++++++++ scripts/iso-image.sh | 44 +++++++--- scripts/iso-install-test.sh | 63 +++++++++++++++ 4 files changed, 263 insertions(+), 11 deletions(-) create mode 100755 scripts/hammer-live-install.sh create mode 100755 scripts/iso-install-test.sh diff --git a/docs/13-release-engineering.md b/docs/13-release-engineering.md index bbf50c02..9dc98fb1 100644 --- a/docs/13-release-engineering.md +++ b/docs/13-release-engineering.md @@ -101,5 +101,24 @@ kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-m 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):** correr el instalador `hammer install` interactivamente desde el live; squashfs+overlay - para no cargar todo el rootfs en RAM; build EFI (el kernel ya trae EFI_STUB, falta poblar la ESP). + (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 ` 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=` + 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.** diff --git a/scripts/hammer-live-install.sh b/scripts/hammer-live-install.sh new file mode 100755 index 00000000..b86136ef --- /dev/null +++ b/scripts/hammer-live-install.sh @@ -0,0 +1,144 @@ +#!/bin/sh +# hammer-install — instalador a disco DESDE el medio live (Etapa E5 / refinamiento #1). +# +# Se inyecta en el rootfs del producto como /usr/bin/hammer-install (ver scripts/iso-image.sh +# INSTALLER=1). Corre DENTRO del live, como root real (arje-zero es PID1) ⇒ NO necesita unshare -r +# ni los rodeos rootless de install-image.sh: particiona, formatea, monta y copia con privilegios +# reales, usando sólo herramientas que el producto ya trae (busybox fdisk/mke2fs/mount/dd + uutils cp). +# +# El sistema que se instala es EL PROPIO LIVE: la raíz en RAM se copia al disco (autoinstalador). El +# payload de arranque (kernel + GRUB) viaja en /usr/lib/hammer/install/, armado en build-time (el host +# tiene grub-mkimage; el live no lo necesita). +# +# Layout en disco (MBR, no GPT — más simple y busybox-friendly; GRUB va en el hueco post-MBR): +# /dev/1 / ext2(montable ext4) + /boot/bzImage + /boot/grub + wrapper /sbin/init +# /dev/2 /store ext2 +# /dev/3 /var/lib/hammer ext2 +# +# Cadena de arranque del disco resultante: BIOS → MBR (boot.img) → core.img (hueco post-MBR) → +# (hd0,msdos1)/boot/grub/grub.cfg → linux /boot/bzImage root=/dev/1 → wrapper /sbin/init. +# +# Uso (en el live): hammer-install /dev/vdb [ROOT_MB] [STORE_MB] +set -eu + +DEV="${1:-/dev/vdb}" +ROOT_MB="${2:-1500}" +STORE_MB="${3:-400}" # /var/lib/hammer toma el resto +PAYLOAD=/usr/lib/hammer/install +BB=/bin/busybox + +[ -b "$DEV" ] || { echo "hammer-install: $DEV no es un block device" >&2; exit 1; } +[ -d "$PAYLOAD" ] || { echo "hammer-install: falta el payload $PAYLOAD (¿ISO sin INSTALLER=1?)" >&2; exit 1; } +[ -r "$PAYLOAD/grub/boot.img" ] && [ -r "$PAYLOAD/grub/core.img" ] || { echo "hammer-install: payload GRUB incompleto" >&2; exit 1; } +[ -r "$PAYLOAD/kernel/bzImage" ] || { echo "hammer-install: falta el kernel en el payload" >&2; exit 1; } + +echo "==> hammer-install: instalando el sistema live en $DEV (root ${ROOT_MB}M, store ${STORE_MB}M, estado=resto)" +echo " ¡esto BORRA $DEV!" + +# --- particionar MBR con busybox fdisk (3 primarias; p1 bootable) --- +# Pasamos sectores de inicio/fin EXPLÍCITOS para las tres: busybox fdisk, al pedir el inicio por +# defecto, ofrece el sector LIBRE MÁS BAJO (= el hueco 63..2047 que dejamos antes de p1) ⇒ con +# colocaría p2 en ese hueco. Por eso computamos los sectores y los tecleamos. +# p1 ARRANCA EN 2048 (1 MiB) A PROPÓSITO: deja un hueco post-MBR de 2047 sectores para el core.img de +# GRUB (~281 sectores). El default CHS de busybox (sector 63 ⇒ hueco de 62) lo DESBORDA, pisando el +# filesystem de p1 ⇒ GRUB cae a `grub>`. +SPM=2048 +P1S=2048; P1E=$(( P1S + ROOT_MB * SPM - 1 )) +P2S=$(( P1E + 1 )); P2E=$(( P2S + STORE_MB * SPM - 1 )) +P3S=$(( P2E + 1 )) # p3 toma hasta el final del disco (último sector = default) +echo "==> particionando $DEV (MBR: p1 ${P1S}-${P1E}, p2 ${P2S}-${P2E}, p3 ${P3S}-fin)" +"$BB" fdisk "$DEV" >/dev/null 2>&1 </dev/null || true +"$BB" sync; sleep 1 + +P1="${DEV}1"; P2="${DEV}2"; P3="${DEV}3" +for p in "$P1" "$P2" "$P3"; do + [ -b "$p" ] || { echo "hammer-install: no apareció la partición $p tras particionar" >&2; exit 1; } +done + +# --- formatear (busybox mke2fs = ext2; el driver ext4 del kernel lo monta) --- +echo "==> formateando particiones (ext2, montables como ext4)" +"$BB" mke2fs -F -L hammer-root "$P1" >/dev/null 2>&1 +"$BB" mke2fs -F -L hammer-store "$P2" >/dev/null 2>&1 +"$BB" mke2fs -F -L hammer-state "$P3" >/dev/null 2>&1 + +# --- copiar la raíz del live a la partición root, excluyendo virtuales + el payload --- +MNT=/run/hammer-install +"$BB" mkdir -p "$MNT" +"$BB" mount -t ext4 "$P1" "$MNT" +echo "==> copiando la raíz del live → $P1 (excluye virtuales/efímeros, /store, /var/lib/hammer y el payload)" +# Enumeramos TODO el top-level de / y copiamos salvo lo virtual/efímero — así no se nos escapa ningún +# dir del producto (p.ej. /ente con la seed.card, /linuxrc). uutils cp -a preserva modos/symlinks. +for entry in /* /.[!.]*; do + [ -e "$entry" ] || continue + name="${entry##*/}" + case "$name" in + proc|sys|dev|run|tmp|store|init) continue ;; # virtuales/efímeros + el /init marcador del live + *) cp -a "$entry" "$MNT/" 2>/dev/null || true ;; + esac +done +# /var sin /var/lib/hammer (estado) — se recrea vacío como mountpoint. +"$BB" rm -rf "$MNT/var/lib/hammer" 2>/dev/null || true +# Quitar el payload del disco instalado (sólo sirve en el live) y recrear mountpoints. +"$BB" rm -rf "$MNT/usr/lib/hammer/install" 2>/dev/null || true +"$BB" mkdir -p "$MNT/store" "$MNT/var/lib/hammer" "$MNT/proc" "$MNT/sys" "$MNT/dev" "$MNT/run" "$MNT/tmp" "$MNT/boot/grub/i386-pc" + +# --- /boot: kernel + módulos GRUB + grub.cfg, y wrapper /sbin/init (monta las particiones dedicadas) --- +echo "==> poblando /boot (kernel + GRUB) y wrapper /sbin/init" +cp "$PAYLOAD/kernel/bzImage" "$MNT/boot/bzImage"; "$BB" chmod 0644 "$MNT/boot/bzImage" +cp "$PAYLOAD"/grub/i386-pc/*.mod "$MNT/boot/grub/i386-pc/" 2>/dev/null || true +cp "$PAYLOAD"/grub/i386-pc/*.lst "$MNT/boot/grub/i386-pc/" 2>/dev/null || true +ROOTDEV="${P1##*/}" # p.ej. vda1 cuando el disco instalado sea el único +cat > "$MNT/boot/grub/grub.cfg" < "$MNT/sbin/init" <<'INIT' +#!/bin/sh +# Wrapper PID1: el kernel monta la partición root; montamos las dedicadas y arrancamos el init real. +/bin/busybox mount -t ext4 /dev/vda2 /store || echo "init: no pude montar /store (/dev/vda2)" +/bin/busybox mount -t ext4 /dev/vda3 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda3)" +exec /usr/bin/arje-zero +INIT +"$BB" chmod +x "$MNT/sbin/init" +"$BB" sync +"$BB" umount "$MNT" + +# --- instalar GRUB: boot.img→MBR (440 B, sin pisar la tabla de particiones) + core.img→hueco post-MBR. +# Con install contiguo MBR los punteros por defecto de GRUB (kernel_sector=1, blocklist=2) ya valen +# ⇒ NO hace falta el parcheo binario que sí pide el caso GPT de install-image.sh. +echo "==> instalando GRUB (boot.img→MBR + core.img→hueco post-MBR)" +"$BB" dd if="$PAYLOAD/grub/boot.img" of="$DEV" bs=1 count=440 conv=notrunc 2>/dev/null +"$BB" dd if="$PAYLOAD/grub/core.img" of="$DEV" bs=512 seek=1 conv=notrunc 2>/dev/null +"$BB" sync + +echo "✓ hammer-install: $DEV instalado. Reiniciá sin el medio live para arrancar del disco." diff --git a/scripts/iso-image.sh b/scripts/iso-image.sh index bea4c77f..52905d8a 100755 --- a/scripts/iso-image.sh +++ b/scripts/iso-image.sh @@ -60,17 +60,43 @@ mkdir -p "$ISO_ROOT/boot/grub/i386-pc" "$RFS" echo "==> staging initramfs desde $ROOTFS (hardlinks) + /init marcador" cp -al "$ROOTFS" "$RFS/root" # El cpio se arma desde $RFS/root; inyectamos /init ahí (rdinit=/init lo busca en la raíz del initramfs). -cat > "$RFS/root/init" <<'INIT' -#!/bin/sh -# /init del medio live (Etapa E5): monta lo básico, deja un marcador inequívoco y arranca el init real. -/bin/busybox mount -t proc proc /proc 2>/dev/null || true -/bin/busybox mount -t sysfs sys /sys 2>/dev/null || true -/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true -echo "HAMMER-ISO-LIVE-OK: medio live arrancado desde El Torito" -exec /sbin/init -INIT +# /init del medio live: monta lo básico + marcador. Con AUTO_INSTALL=, instala desatendido a ese +# disco y apaga (instalación unattended + scaffolding del test); si no, arranca el init real (live). +{ + echo '#!/bin/sh' + echo '# /init del medio live (Etapa E5).' + echo '/bin/busybox mount -t proc proc /proc 2>/dev/null || true' + echo '/bin/busybox mount -t sysfs sys /sys 2>/dev/null || true' + echo '/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true' + echo 'echo "HAMMER-ISO-LIVE-OK: medio live arrancado desde El Torito"' + if [ -n "${AUTO_INSTALL:-}" ]; then + echo "echo 'HAMMER-AUTO-INSTALL: instalando a ${AUTO_INSTALL}'" + echo "if /usr/bin/hammer-install '${AUTO_INSTALL}'; then echo 'HAMMER-INSTALL-OK'; else echo 'HAMMER-INSTALL-FAIL'; fi" + echo '/bin/busybox sync' + echo '/bin/busybox poweroff -f' + else + echo 'exec /sbin/init' + fi +} > "$RFS/root/init" chmod +x "$RFS/root/init" +# --- INSTALLER=1: payload de instalación a disco DESDE el live (refinamiento #1) --- +# Bundle kernel + GRUB (boot.img + core.img MBR-prefix + módulos) bajo /usr/lib/hammer/install/ y el +# instalador en /usr/bin/hammer-install. El core.img se arma acá (build-time, el host tiene +# grub-mkimage); el live NO lo necesita — sólo dd's. core prefix (hd0,msdos1)/boot/grub para boot MBR. +if [ "${INSTALLER:-0}" = 1 ]; then + echo "==> INSTALLER=1: bundling payload de instalación a disco (kernel + GRUB MBR + hammer-install)" + IPL="$RFS/root/usr/lib/hammer/install" + mkdir -p "$IPL/grub/i386-pc" "$IPL/kernel" + cp "$KERNEL" "$IPL/kernel/bzImage"; chmod 0644 "$IPL/kernel/bzImage" + cp "$GRUB_LIB/boot.img" "$IPL/grub/boot.img" + cp "$GRUB_LIB"/*.mod "$GRUB_LIB"/*.lst "$IPL/grub/i386-pc/" 2>/dev/null || true + # core.img para disco MBR: prefix a la 1ª partición msdos, módulos de disco (biosdisk/part_msdos/ext2). + grub-mkimage -O i386-pc -p '(hd0,msdos1)/boot/grub' -o "$IPL/grub/core.img" \ + biosdisk part_msdos ext2 normal configfile linux echo ls boot search + install -m 0755 "$ROOT/scripts/hammer-live-install.sh" "$RFS/root/usr/bin/hammer-install" +fi + echo "==> empaquetando initramfs.cpio.gz" ( cd "$RFS/root" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$ISO_ROOT/boot/initramfs.cpio.gz" diff --git a/scripts/iso-install-test.sh b/scripts/iso-install-test.sh new file mode 100755 index 00000000..9b62d71f --- /dev/null +++ b/scripts/iso-install-test.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# iso-install-test.sh — valida el lazo COMPLETO de instalación desde el medio live (refinamiento #1): +# (1) arma un ISO instalador (iso-image.sh INSTALLER=1 AUTO_INSTALL=/dev/vda) con el payload de disco +# (kernel + GRUB MBR + /usr/bin/hammer-install) bundleado; +# (2) lo bootea con un disco EN BLANCO ⇒ el /init desatendido corre hammer-install y apaga; +# (3) bootea ESE disco SOLO (sin ISO, sin -kernel) ⇒ el producto instalado arranca y sirve SSH. +# Cierra el lazo "ISO live → disco instalado → bootea solo", lo que hace la distro distribuible. +# +# Uso: PRODUCT= ./scripts/iso-install-test.sh +# Vars: KERNEL MEM (def 4096) KVM (def 1) DISK_MB (def 4096) INSTALL_DEADLINE (def 180) BOOT_DEADLINE (def 90) +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" +KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}" +MEM="${MEM:-4096}"; KVM="${KVM:-1}"; DISK_MB="${DISK_MB:-4096}" +INSTALL_DEADLINE="${INSTALL_DEADLINE:-180}"; BOOT_DEADLINE="${BOOT_DEADLINE:-90}" +WORK="$ROOT/work/iso-install-test"; ISO="$WORK/installer.iso"; TGT="$WORK/target.img" +ILOG="$WORK/install.log"; BLOG="$WORK/diskboot.log" +mkdir -p "$WORK" + +if [ -n "${PRODUCT:-}" ]; then + PDIR=$(ls -d store/"${PRODUCT#b3:}"*-product-rootfs 2>/dev/null | head -1) +else + PDIR=$(ls -dt store/*-product-rootfs 2>/dev/null | head -1) +fi +[ -n "$PDIR" ] && [ -d "$PDIR" ] || { echo "no encuentro product-rootfs"; exit 1; } +[ -n "$KERNEL" ] && [ -e "$KERNEL" ] || { echo "no encuentro kernel"; exit 1; } +echo "==> product-rootfs: $PDIR" + +accel=(); if [ "$KVM" = 1 ] && [ -w /dev/kvm ]; then accel=(-enable-kvm -cpu host); fi + +echo "==> (1) armando ISO instalador (INSTALLER=1, AUTO_INSTALL=/dev/vda)" +ROOTFS="$PDIR" ISO="$ISO" KERNEL="$KERNEL" INSTALLER=1 AUTO_INSTALL=/dev/vda ./scripts/iso-image.sh >/dev/null +echo " ISO: $(du -h "$ISO" | cut -f1)" + +echo "==> (2) boot del instalador con disco EN BLANCO ${DISK_MB}M (auto-instala y apaga)" +rm -f "$TGT"; truncate -s "${DISK_MB}M" "$TGT" +timeout "$INSTALL_DEADLINE" qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \ + -drive file="$TGT",format=raw,if=virtio -cdrom "$ISO" -boot d > "$ILOG" 2>&1 || true +if ! grep -q "HAMMER-INSTALL-OK" "$ILOG"; then + echo "✗ la instalación NO terminó OK — ver $ILOG" >&2 + grep -iE "HAMMER-INSTALL-FAIL|hammer-install:|no apareció|error" "$ILOG" | head; exit 1 +fi +echo " ✓ HAMMER-INSTALL-OK" + +echo "==> (3) boot del DISCO instalado (sin ISO, sin -kernel)" +timeout "$BOOT_DEADLINE" qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \ + -drive file="$TGT",format=raw,if=virtio > "$BLOG" 2>&1 || true + +fail=0 +check() { if grep -qiE "$1" "$BLOG"; then echo " ✓ $2"; else echo " ✗ $2" >&2; fail=1; fi; } +reject() { if grep -qiE "$1" "$BLOG"; then echo " ✗ $2 (apareció)" >&2; fail=1; else echo " ✓ $2"; fi; } +echo "==> asserts sobre el arranque del disco:" +check "GNU GRUB" "GRUB del disco (MBR→core.img del hueco post-MBR)" +check "Linux version 6\.16" "kernel cargado del disco" +check "Server listening on .* port 22" "sshd escuchando (producto instalado en servicio)" +reject "ARRANQUE FALLIDO|seed.card no encontrada" "arje-zero arrancó sin fallo de seed" +reject "no pude montar" "particiones dedicadas /store y /var/lib/hammer montadas" + +if [ "$fail" = 0 ]; then + echo; echo "✓✓ INSTALL-FROM-LIVE VERDE: ISO live → hammer-install → disco que bootea solo y sirve SSH" +else + echo; echo "✗ install-from-live FALLÓ — ver $ILOG / $BLOG" >&2; exit 1 +fi