auto-recover al arranque: crate hammer-recover + hook /sbin/init (E4 #4b)
El modelo de generaciones es in-place (sin menu NixOS que ofrecer en GRUB); lo que encaja es auto-sanar un upgrade interrumpido al boot. crates/hammer-recover = mini-binario static-musl (lo unico que el producto necesita, sin el CLI hammer completo): sin pending.json es no-op; con uno completa (roll-forward) o deshace (roll-back) -> FHS siempre consistente; nunca aborta el boot. iso-image.sh INSTALLER=1 lo compila (target musl) y bundlea al payload; hammer-live-install.sh lo copia a /usr/sbin/hammer-recover y el wrapper /sbin/init instalado lo corre tras montar /store y /var/lib/hammer, antes de incarnar arje-zero. iso-install- test.sh valida el hook al boot (marker 'hammer-recover: sin upgrade interrumpido'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+8
@@ -614,6 +614,14 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hammer-recover"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"hammer-journal",
|
||||||
|
"hammer-upgrade",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hammer-upgrade"
|
name = "hammer-upgrade"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"crates/hammer-journal",
|
"crates/hammer-journal",
|
||||||
"crates/hammer-mirror",
|
"crates/hammer-mirror",
|
||||||
"crates/hammer-upgrade",
|
"crates/hammer-upgrade",
|
||||||
|
"crates/hammer-recover",
|
||||||
"crates/hammer-agent",
|
"crates/hammer-agent",
|
||||||
"crates/hammer-cli",
|
"crates/hammer-cli",
|
||||||
"crates/hammerd",
|
"crates/hammerd",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "hammer-recover"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = "Mini-binario de auto-recuperación de upgrades al arranque (E4 #4b): el /sbin/init lo corre para completar/deshacer un apply interrumpido antes de incarnar."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "hammer-recover"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hammer-upgrade.workspace = true
|
||||||
|
hammer-journal.workspace = true
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! `hammer-recover` — auto-recuperación de upgrades al **arranque** (E4 / refinamiento #4b).
|
||||||
|
//!
|
||||||
|
//! El producto NO lleva el CLI `hammer` completo; este mini-binario (estático musl) es lo único que el
|
||||||
|
//! sistema instalado necesita para auto-sanar un upgrade interrumpido. El wrapper `/sbin/init` lo corre
|
||||||
|
//! **tras montar `/store` y `/var/lib/hammer`** y **antes** de incarnar arje-zero:
|
||||||
|
//!
|
||||||
|
//! - Sin intento pendiente ⇒ no-op (el caso normal).
|
||||||
|
//! - Con un `pending.json` (un `hammer upgrade apply` cortado por un apagón/reinicio): intenta
|
||||||
|
//! **completar** (roll-forward, re-ejecuta el plan idempotente + commitea); si no puede (p.ej. el árbol
|
||||||
|
//! ya no está en `/store`), **deshace** (roll-back) para dejar el FHS consistente. En cualquier caso el
|
||||||
|
//! sistema arranca en un estado coherente, no a medias.
|
||||||
|
//!
|
||||||
|
//! Encaja con el modelo de generaciones **in-place** (el FHS es la generación viva): no hay menú de
|
||||||
|
//! generaciones tipo NixOS que seleccionar, sino una reparación determinista del estado a medias.
|
||||||
|
//!
|
||||||
|
//! Paths del producto (overridables por entorno para tests):
|
||||||
|
//! `HAMMER_RECOVER_ROOT=/` `HAMMER_RECOVER_STORE=/store`
|
||||||
|
//! `HAMMER_RECOVER_STATE=/var/lib/hammer/upgrades` `HAMMER_RECOVER_JOURNAL=/var/lib/hammer/journal`
|
||||||
|
//! Flag: `--rollback` fuerza deshacer en vez de completar.
|
||||||
|
//!
|
||||||
|
//! NUNCA sale con código ≠0 por un fallo de recuperación: es un hook de arranque, no debe abortar el
|
||||||
|
//! boot. Reporta por consola y deja que arje-zero siga (con el estado más consistente que logró).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn env_path(key: &str, default: &str) -> PathBuf {
|
||||||
|
std::env::var(key).map(PathBuf::from).unwrap_or_else(|_| PathBuf::from(default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let force_rollback = std::env::args().skip(1).any(|a| a == "--rollback");
|
||||||
|
let root = env_path("HAMMER_RECOVER_ROOT", "/");
|
||||||
|
let store = env_path("HAMMER_RECOVER_STORE", "/store");
|
||||||
|
let state = env_path("HAMMER_RECOVER_STATE", "/var/lib/hammer/upgrades");
|
||||||
|
let journal_dir = env_path("HAMMER_RECOVER_JOURNAL", "/var/lib/hammer/journal");
|
||||||
|
|
||||||
|
match hammer_upgrade::pending(&state) {
|
||||||
|
Ok(None) => {
|
||||||
|
println!("hammer-recover: sin upgrade interrumpido");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(Some(p)) => {
|
||||||
|
println!("hammer-recover: upgrade interrumpido (generación {}) — recuperando", p.id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("hammer-recover: no pude leer el estado de upgrades: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let journal = hammer_journal::Journal::open(&journal_dir).ok();
|
||||||
|
let j = journal.as_ref();
|
||||||
|
|
||||||
|
if force_rollback {
|
||||||
|
match hammer_upgrade::recover(&store, &root, &state, j, true) {
|
||||||
|
Ok(r) => println!("hammer-recover: DESHECHO (generación {} descartada)", r.generation),
|
||||||
|
Err(e) => eprintln!("hammer-recover: rollback falló: {e}"),
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimista: completar el upgrade. Si el roll-forward falla, deshacer ⇒ FHS consistente igual.
|
||||||
|
match hammer_upgrade::recover(&store, &root, &state, j, false) {
|
||||||
|
Ok(r) => println!("hammer-recover: COMPLETADO (generación {})", r.generation),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("hammer-recover: roll-forward falló ({e}); deshaciendo para dejar el FHS consistente");
|
||||||
|
match hammer_upgrade::recover(&store, &root, &state, j, true) {
|
||||||
|
Ok(r) => println!("hammer-recover: DESHECHO tras fallo (generación {})", r.generation),
|
||||||
|
Err(e2) => eprintln!("hammer-recover: rollback de respaldo también falló: {e2}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,8 +91,17 @@ kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-m
|
|||||||
re-ejecuta el plan + commitea, re-verificando el `of_tree`) o **deshace** (roll-back: restaura backups,
|
re-ejecuta el plan + commitea, re-verificando el `of_tree`) o **deshace** (roll-back: restaura backups,
|
||||||
borra la generación a medias, `current`→padre). `apply` se **niega** si hay intento pendiente (exige
|
borra la generación a medias, `current`→padre). `apply` se **niega** si hay intento pendiente (exige
|
||||||
recover explícito); `status` lo avisa. 5 tests (corte a media proyección → ambos modos) + ejercicio en
|
recover explícito); `status` lo avisa. 5 tests (corte a media proyección → ambos modos) + ejercicio en
|
||||||
`upgrade-e2e-test.sh`. **Endurecimiento pendiente:** cableado del init de boot para que rollback/recover
|
`upgrade-e2e-test.sh`.
|
||||||
sea seleccionable al arranque (necesita el binario `hammer` en el producto).
|
- **Auto-recover al arranque ✅** (`crates/hammer-recover` — mini-binario static-musl). El modelo de
|
||||||
|
generaciones es **in-place** (el FHS es la generación viva), así que NO hay menú de generaciones tipo
|
||||||
|
NixOS que ofrecer en GRUB; lo que encaja es **auto-sanar** un upgrade interrumpido al boot. El producto
|
||||||
|
no lleva el CLI `hammer` completo ⇒ `hammer-recover` es lo único que el sistema instalado necesita: el
|
||||||
|
wrapper `/sbin/init` (de `hammer-live-install.sh`) lo corre **tras montar `/store` y `/var/lib/hammer`**
|
||||||
|
y **antes** de incarnar arje-zero. Sin `pending.json` es no-op; con uno, **completa** (roll-forward) o,
|
||||||
|
si no puede, **deshace** (roll-back) ⇒ el FHS siempre arranca consistente. Nunca aborta el boot. El
|
||||||
|
instalador lo bundlea desde el payload a `/usr/sbin/hammer-recover`. Validado in-VM
|
||||||
|
(`iso-install-test.sh`): el disco imprime `hammer-recover: sin upgrade interrumpido` al boot y sirve SSH.
|
||||||
|
**Refinamiento #4 (E4 endurecimiento) CERRADO.**
|
||||||
- **E5 — ISO/medio de arranque: ✅ primer corte** (`recipes/xorriso.toml` + `scripts/iso-image.sh` +
|
- **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
|
`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
|
kernel + initramfs desde el ISO 9660, el kernel desempaqueta el rootfs del producto a un tmpfs y corre
|
||||||
|
|||||||
@@ -121,12 +121,20 @@ menuentry "hammer" {
|
|||||||
linux /boot/bzImage console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init
|
linux /boot/bzImage console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init
|
||||||
}
|
}
|
||||||
CFG
|
CFG
|
||||||
|
# hammer-recover: auto-sana un upgrade interrumpido al arranque (E4 #4b). Lo dejamos en /usr/sbin del
|
||||||
|
# disco; el wrapper lo corre tras montar /store y /var/lib/hammer (que necesita) y antes de incarnar.
|
||||||
|
if [ -x "$PAYLOAD/hammer-recover" ]; then
|
||||||
|
"$BB" mkdir -p "$MNT/usr/sbin"
|
||||||
|
cp "$PAYLOAD/hammer-recover" "$MNT/usr/sbin/hammer-recover"; "$BB" chmod 0755 "$MNT/usr/sbin/hammer-recover"
|
||||||
|
fi
|
||||||
"$BB" rm -f "$MNT/sbin/init"
|
"$BB" rm -f "$MNT/sbin/init"
|
||||||
cat > "$MNT/sbin/init" <<'INIT'
|
cat > "$MNT/sbin/init" <<'INIT'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Wrapper PID1: el kernel monta la partición root; montamos las dedicadas y arrancamos el init real.
|
# Wrapper PID1: el kernel monta la partición root; montamos las dedicadas, auto-sanamos un upgrade
|
||||||
|
# interrumpido (si lo hay) 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/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)"
|
/bin/busybox mount -t ext4 /dev/vda3 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda3)"
|
||||||
|
[ -x /usr/sbin/hammer-recover ] && /usr/sbin/hammer-recover || true
|
||||||
exec /usr/bin/arje-zero
|
exec /usr/bin/arje-zero
|
||||||
INIT
|
INIT
|
||||||
"$BB" chmod +x "$MNT/sbin/init"
|
"$BB" chmod +x "$MNT/sbin/init"
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ if [ "${INSTALLER:-0}" = 1 ]; then
|
|||||||
grub-mkimage -O i386-pc -p '(hd0,msdos1)/boot/grub' -o "$IPL/grub/core.img" \
|
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
|
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"
|
install -m 0755 "$ROOT/scripts/hammer-live-install.sh" "$RFS/root/usr/bin/hammer-install"
|
||||||
|
# hammer-recover (static musl): auto-sana un upgrade interrumpido al arranque del sistema instalado.
|
||||||
|
RECOVER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer-recover"
|
||||||
|
if [ ! -x "$RECOVER_BIN" ]; then
|
||||||
|
echo "==> compilando hammer-recover (static musl) para el payload"
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl -p hammer-recover --manifest-path "$ROOT/Cargo.toml" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
[ -x "$RECOVER_BIN" ] || { echo "no pude construir hammer-recover (target musl?)" >&2; exit 1; }
|
||||||
|
install -m 0755 "$RECOVER_BIN" "$IPL/hammer-recover"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> empaquetando initramfs.cpio.gz"
|
echo "==> empaquetando initramfs.cpio.gz"
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ echo "==> asserts sobre el arranque del disco:"
|
|||||||
check "GNU GRUB" "GRUB del disco (MBR→core.img del hueco post-MBR)"
|
check "GNU GRUB" "GRUB del disco (MBR→core.img del hueco post-MBR)"
|
||||||
check "Linux version 6\.16" "kernel cargado del disco"
|
check "Linux version 6\.16" "kernel cargado del disco"
|
||||||
check "Server listening on .* port 22" "sshd escuchando (producto instalado en servicio)"
|
check "Server listening on .* port 22" "sshd escuchando (producto instalado en servicio)"
|
||||||
|
check "hammer-recover:" "hook de auto-recover de upgrades corrió al arranque (#4b)"
|
||||||
reject "ARRANQUE FALLIDO|seed.card no encontrada" "arje-zero arrancó sin fallo de seed"
|
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"
|
reject "no pude montar" "particiones dedicadas /store y /var/lib/hammer montadas"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user