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:
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user