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>
This commit is contained in:
2026-06-20 21:46:14 -04:00
co-authored by Claude Opus 4.8
parent 30daab35e6
commit d23946fa3d
8 changed files with 953 additions and 2 deletions
+1
View File
@@ -22,6 +22,7 @@ hammer-core.workspace = true
hammer-build.workspace = true
hammer-bootstrap.workspace = true
hammer-mirror.workspace = true
hammer-upgrade.workspace = true
hammer-overlay.workspace = true
hammer-journal.workspace = true
hammer-agent.workspace = true
+75
View File
@@ -245,6 +245,38 @@ enum Cmd {
#[command(subcommand)]
sub: MirrorCmd,
},
/// [Etapa E4] Upgrades atómicos con rollback: aplica un árbol Stage1/producto del store al root
/// vivo dejando una generación nueva; `rollback` revierte exactamente al árbol anterior.
Upgrade {
/// Root del FHS vivo sobre el que se proyecta el árbol (default `/`).
#[arg(long, default_value = "/", global = true)]
root: String,
/// Directorio de estado de generaciones (default `/var/lib/hammer/upgrades`).
#[arg(long, default_value = hammer_upgrade::DEFAULT_STATE_ROOT, global = true)]
state_root: String,
/// Diario donde registrar cada cambio (default `/var/lib/hammer/journal`).
#[arg(long, default_value = "/var/lib/hammer/journal", global = true)]
journal: String,
#[command(subcommand)]
sub: UpgradeCmd,
},
}
#[derive(Subcommand)]
enum UpgradeCmd {
/// Aplica el árbol `<tree-dir>` (`<hash>-<name>` del store) al root vivo, dejando una generación.
Apply {
/// Directorio del artefacto en el store (p.ej. `b3hex…-product-rootfs`).
tree_dir: String,
/// `of_tree` esperado (`b3:…`) — de un índice de mirror o release firmada; si se da, se exige.
#[arg(long)]
expected: Option<String>,
},
/// Revierte la generación viva al árbol anterior (o al estado pre-upgrades).
Rollback,
/// Muestra la generación viva y el historial.
Status,
}
#[derive(Subcommand)]
@@ -798,6 +830,49 @@ fn main() -> anyhow::Result<()> {
}
}
}
Cmd::Upgrade { root, state_root, journal, sub } => {
let store = std::path::PathBuf::from(&cli.store);
let root = std::path::PathBuf::from(&root);
let state_root = std::path::PathBuf::from(&state_root);
match sub {
UpgradeCmd::Apply { tree_dir, expected } => {
let j = hammer_journal::Journal::open(&journal)?;
let r = hammer_upgrade::apply(
&store, &tree_dir, expected.as_deref(), &root, &state_root, Some(&j),
)?;
if r.already_current {
println!("generación {} ya viva — árbol idéntico, no-op", r.generation);
} else {
println!("✓ generación {} aplicada", r.generation);
println!(" + {} añadidos, ~ {} pisados, - {} retirados",
r.added.len(), r.replaced.len(), r.removed.len());
}
}
UpgradeCmd::Rollback => {
let j = hammer_journal::Journal::open(&journal)?;
let r = hammer_upgrade::rollback(&root, &state_root, Some(&j))?;
println!("✓ generación {} revertida", r.reverted);
match r.now_current {
Some(id) => println!(" ahora viva: generación {id}"),
None => println!(" ahora viva: estado pre-upgrades"),
}
println!(" {} restaurados, {} borrados", r.restored.len(), r.deleted.len());
}
UpgradeCmd::Status => {
let cur = hammer_upgrade::current(&state_root)?;
match cur {
Some(id) => println!("generación viva: {id}"),
None => println!("generación viva: (ninguna — sistema sin upgrades aplicados)"),
}
let gens = hammer_upgrade::list(&state_root)?;
println!("historial: {} generación(es)", gens.len());
for g in &gens {
let mark = if Some(g.id) == cur { "*" } else { " " };
println!(" {mark} {}{} ({} ficheros)", g.id, g.tree_dir, g.files.len());
}
}
}
}
}
Ok(())
}