Axe 8 (sécurité) : - am audit : checksums SHA-256 des configs (embarquée, utilisateur, locale) stockés dans state.json, croisement avec events.jsonl pour dater les changements, détection des modifications manuelles hors de am (statut MANUAL), rapport lisible + --json stable (contrat) - backup automatique avant chaque am update (agent ou --all) : état + config utilisateur + répertoires d'installation + shims dans <state>/backups/<ts>/ avec manifest.json - am update --rollback [point] : restaure le dernier backup (ou un id précis, préfixe accepté), arrête les agents concernés d'abord, événement EventKind::Rollback journalisé - am update --rollback list : liste les points de restauration - rétention settings.backups_keep (défaut 5) appliquée après chaque backup - événements EventKind::Backup / EventKind::Rollback pour l'audit REPL : commande audit + update --rollback (parse, complétion), help specs, tip cheat sheet, man pages régénérées (50 pages). Version 0.4.6, ROADMAP cases cochées, 272 tests verts.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
//! reinstall when an update exists.
|
||||
|
||||
use super::*;
|
||||
use crate::backup;
|
||||
use crate::events::{Event, EventKind};
|
||||
use crate::installers;
|
||||
use crate::runner::{Runner, SystemRunner};
|
||||
@@ -9,7 +10,17 @@ use anyhow::{bail, Result};
|
||||
use std::cmp::Ordering;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn run(app: &App, agent_name: Option<&str>, all: bool) -> Result<i32> {
|
||||
pub fn run(
|
||||
app: &App,
|
||||
agent_name: Option<&str>,
|
||||
all: bool,
|
||||
rollback: Option<&str>,
|
||||
) -> Result<i32> {
|
||||
// Issue #64: --rollback restores a pre-update backup instead of updating.
|
||||
if rollback.is_some() {
|
||||
return rollback_point(app, rollback);
|
||||
}
|
||||
|
||||
let runner = SystemRunner::new(app.dry_run(), app.cli.verbose, &app.log);
|
||||
|
||||
let policy = app.config.settings.update_policy.unwrap_or_default();
|
||||
@@ -28,6 +39,16 @@ pub fn run(app: &App, agent_name: Option<&str>, all: bool) -> Result<i32> {
|
||||
let entries = app.state.all()?;
|
||||
let names: Vec<String> = entries.keys().cloned().collect();
|
||||
let mut failed = 0;
|
||||
// Issue #64: one snapshot covers the whole batch.
|
||||
if !app.dry_run() && !names.is_empty() {
|
||||
let info = backup::create_backup(app, "update:--all", &names)?;
|
||||
app.log.verbose(&format!("pre-update backup: {}", info.id));
|
||||
app.emit(
|
||||
&Event::now(EventKind::Backup)
|
||||
.with_args(vec![info.id.clone()])
|
||||
.with_reason("update:--all"),
|
||||
);
|
||||
}
|
||||
for name in names {
|
||||
if let Err(e) = update_one(app, &runner, &name) {
|
||||
failed += 1;
|
||||
@@ -124,6 +145,16 @@ fn update_one(app: &App, runner: &dyn Runner, name: &str) -> Result<i32> {
|
||||
.dry(format!("would reinstall {} via {}", agent.name, method.kind));
|
||||
return Ok(0);
|
||||
}
|
||||
// Issue #64: snapshot before the real update so a broken upgrade can be
|
||||
// undone with 'am update --rollback'.
|
||||
let info = backup::create_backup(app, &format!("update:{}", agent.name), &[agent.name.clone()])?;
|
||||
app.log.verbose(&format!("pre-update backup: {}", info.id));
|
||||
app.emit(
|
||||
&Event::now(EventKind::Backup)
|
||||
.with_args(vec![info.id.clone()])
|
||||
.with_agent(agent.name.clone())
|
||||
.with_reason("pre-update"),
|
||||
);
|
||||
crate::deps::ensure_deps(app, agent, runner)?;
|
||||
let outcome = installers::run_install(app, agent, method, runner)?;
|
||||
let mut new_entry = installers::make_entry(agent, method, &outcome, app);
|
||||
@@ -147,3 +178,42 @@ fn update_one(app: &App, runner: &dyn Runner, name: &str) -> Result<i32> {
|
||||
));
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Issue #64: 'am update --rollback [point]' — restore a pre-update backup.
|
||||
/// '--rollback list' prints the available backup points.
|
||||
fn rollback_point(app: &App, point: Option<&str>) -> Result<i32> {
|
||||
if point == Some("list") {
|
||||
let all = backup::list_backups(app);
|
||||
if all.is_empty() {
|
||||
app.log.info("no backup found — run an update first to create one");
|
||||
return Ok(0);
|
||||
}
|
||||
println!("pre-update backups ({}):", all.len());
|
||||
for b in &all {
|
||||
println!(
|
||||
" {} {} agents={}{}",
|
||||
b.id,
|
||||
b.reason,
|
||||
if b.agents.is_empty() {
|
||||
"all".to_string()
|
||||
} else {
|
||||
b.agents.join(",")
|
||||
},
|
||||
if b.state && b.config { " [state+config]" } else { "" }
|
||||
);
|
||||
}
|
||||
println!("\nuse 'am update --rollback <id>' to restore one");
|
||||
return Ok(0);
|
||||
}
|
||||
let info = backup::restore_backup(app, point)?;
|
||||
app.emit(
|
||||
&Event::now(EventKind::Rollback)
|
||||
.with_args(vec![info.id.clone()])
|
||||
.with_reason(&info.reason),
|
||||
);
|
||||
app.log.success(&format!(
|
||||
"rolled back to backup {} ({})",
|
||||
info.id, info.reason
|
||||
));
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user