nixfleet_agent/activation/
rollback.rs1use anyhow::{Context, Result};
8use tokio::process::Command;
9
10use super::profile::resolve_profile_target;
11use super::types::ActivationBackend;
12use super::types::RollbackOutcome;
13use super::verify_poll::{PollOutcome, VerifyPoll};
14
15pub async fn rollback_with<B: ActivationBackend>(backend: &B) -> Result<RollbackOutcome> {
16 tracing::warn!("agent: triggering local rollback (fire-and-forget via systemd-run)");
17
18 let env_status = Command::new("nix-env")
19 .arg("--profile")
20 .arg("/nix/var/nix/profiles/system")
21 .arg("--rollback")
22 .status()
23 .await
24 .with_context(|| "spawn nix-env --rollback")?;
25 if !env_status.success() {
26 tracing::error!(
27 exit_code = ?env_status.code(),
28 "agent: nix-env --rollback failed; cannot proceed",
29 );
30 return Ok(RollbackOutcome::Failed {
31 phase: "nix-env-rollback".to_string(),
32 exit_code: env_status.code(),
33 });
34 }
35
36 let target_basename = match resolve_profile_target() {
38 Ok(b) => b,
39 Err(err) => {
40 tracing::error!(
41 error = %err,
42 "agent: cannot resolve rolled-back profile target; aborting rollback",
43 );
44 return Ok(RollbackOutcome::Failed {
45 phase: "discover-target".to_string(),
46 exit_code: None,
47 });
48 }
49 };
50 tracing::info!(
51 target_basename = %target_basename,
52 "agent: rollback target discovered; firing detached switch",
53 );
54
55 if let Some(failure) = backend.fire_rollback(&target_basename).await? {
56 return Ok(failure);
57 }
58
59 match VerifyPoll::new(&target_basename).until_settled().await {
62 PollOutcome::Settled => {
63 tracing::info!(
64 target = %target_basename,
65 "agent: rollback fire-and-forget complete",
66 );
67 Ok(RollbackOutcome::FiredAndPolled {
68 reverted_to_closure: target_basename,
69 })
70 }
71 PollOutcome::Timeout { last_observed } => {
72 let exit_code = backend
73 .read_unit_exit_code("nixfleet-rollback.service")
74 .await;
75 tracing::error!(
76 target = %target_basename,
77 last_observed = %last_observed,
78 exit_code = ?exit_code,
79 "agent: rollback poll timed out",
80 );
81 Ok(RollbackOutcome::Failed {
82 phase: "rollback-poll-timeout".to_string(),
83 exit_code,
84 })
85 }
86 PollOutcome::FlippedToUnexpected { .. } => {
87 unreachable!(
88 "FlippedToUnexpected requires Some(previous_basename); rollback leaves it None"
89 )
90 }
91 }
92}