Files
takana/crates/takana-recover/src/main.rs
T
Sergio b818f5249f takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).

EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.

El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:

  b"hammer-tree-v1"        <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
  b"hammer-seed-v1"           la funcion que hashea TODOS los artefactos:
  b"hammer-stage1-rootfs-v2"  cambiarla mueve los 4750 hashes del store
  b"hammer-product-rootfs-v3"
  b"hammer-product-attested-v2"
  b"hammer-builder-rootfs-v1"
  b"hammer-attest-dev-rootkey-0001!!"  <- clave raiz de atestacion, [u8;32]

Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.

Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.

La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
2026-09-09 19:38:33 +00:00

84 lines
4.0 KiB
Rust

//! `hammer-recover` — auto-recuperación de upgrades al **arranque** (E4 / refinamiento #4b).
//!
//! El producto NO lleva el CLI `takana` 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 `takana 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):
//! `TAKANA_RECOVER_ROOT=/` `TAKANA_RECOVER_STORE=/store`
//! `TAKANA_RECOVER_STATE=/var/lib/hammer/upgrades` `TAKANA_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;
/// Lee la variable aceptando el nombre nuevo y, si falta, el viejo (ADR 0016).
///
/// La caída va inline y no por `takana_core::env`: este es un mini-binario que se copia a
/// `/usr/sbin` de los sistemas instalados, y no vale la pena arrastrarle una dep entera por
/// dos líneas. Se retira junto con el resto de las caídas en la etapa 6.
fn env_path(key: &str, default: &str) -> PathBuf {
let viejo = key.strip_prefix("TAKANA_").map(|s| format!("HAMMER_{s}"));
std::env::var(key)
.ok()
.or_else(|| viejo.and_then(|v| std::env::var(v).ok()))
.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("TAKANA_RECOVER_ROOT", "/");
let store = env_path("TAKANA_RECOVER_STORE", "/store");
let state = env_path("TAKANA_RECOVER_STATE", "/var/lib/hammer/upgrades");
let journal_dir = env_path("TAKANA_RECOVER_JOURNAL", "/var/lib/hammer/journal");
match takana_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 = takana_journal::Journal::open(&journal_dir).ok();
let j = journal.as_ref();
if force_rollback {
match takana_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 takana_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 takana_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}"),
}
}
}
}