Skip to main content

consortium_nix/
activate.rs

1//! Profile activation — switch NixOS/nix-darwin systems to new configurations.
2
3use std::collections::HashMap;
4use std::process::Command;
5
6use crate::config::{DeployAction, DeploymentPlan, ProfileType};
7use crate::error::{NixError, Result};
8
9/// Activation results keyed by hostname.
10pub struct ActivationResults {
11    /// Hosts that were successfully activated.
12    pub succeeded: Vec<String>,
13    /// Map of hostname -> activation error.
14    pub errors: HashMap<String, NixError>,
15}
16
17/// Activate profiles on all targets in the deployment plan.
18pub fn activate_all(plan: &DeploymentPlan) -> Result<ActivationResults> {
19    let mut results = ActivationResults {
20        succeeded: Vec::new(),
21        errors: HashMap::new(),
22    };
23
24    if plan.action == DeployAction::Build {
25        // Build-only mode, skip activation
26        results.succeeded = plan.targets.iter().map(|t| t.node.name.clone()).collect();
27        return Ok(results);
28    }
29
30    // TODO: parallelize with consortium's SshWorker + fanout
31    // TODO: support rolling activation (sequential with health checks)
32    for target in &plan.targets {
33        match activate_host(
34            &target.node.target_host,
35            &target.node.target_user,
36            &target.toplevel_path,
37            &target.node.profile_type,
38            plan.action,
39        ) {
40            Ok(()) => {
41                results.succeeded.push(target.node.name.clone());
42            }
43            Err(e) => {
44                results.errors.insert(target.node.name.clone(), e);
45            }
46        }
47    }
48
49    Ok(results)
50}
51
52/// Activate a profile on a single host.
53pub fn activate_host(
54    host: &str,
55    user: &str,
56    toplevel_path: &str,
57    profile_type: &ProfileType,
58    action: DeployAction,
59) -> Result<()> {
60    // Only set the system profile for actions that should persist across reboots.
61    // dry-activate and test should NOT modify the profile.
62    match action {
63        DeployAction::Switch | DeployAction::Boot => {
64            set_profile(host, user, toplevel_path)?;
65        }
66        DeployAction::Test | DeployAction::DryActivate | DeployAction::Build => {}
67    }
68
69    // Run the activation command
70    let activation_cmd = match profile_type {
71        ProfileType::Nixos => {
72            format!("{}/bin/switch-to-configuration {}", toplevel_path, action)
73        }
74        ProfileType::NixDarwin => {
75            format!(
76                "{}/activate-user && sudo {}/activate",
77                toplevel_path, toplevel_path
78            )
79        }
80    };
81
82    let output = Command::new("ssh")
83        .args([
84            "-oStrictHostKeyChecking=no",
85            "-oPasswordAuthentication=no",
86            "-oConnectTimeout=30",
87            "-l",
88            user,
89            host,
90            &activation_cmd,
91        ])
92        .output()
93        .map_err(|e| NixError::ActivationFailed {
94            host: host.to_string(),
95            message: format!("failed to run activation: {}", e),
96        })?;
97
98    if !output.status.success() {
99        let stderr = String::from_utf8_lossy(&output.stderr);
100        return Err(NixError::ActivationFailed {
101            host: host.to_string(),
102            message: stderr.to_string(),
103        });
104    }
105
106    Ok(())
107}
108
109/// Set the nix profile to point to the new system closure.
110fn set_profile(host: &str, user: &str, toplevel_path: &str) -> Result<()> {
111    let cmd = format!(
112        "nix-env -p /nix/var/nix/profiles/system --set {}",
113        toplevel_path
114    );
115
116    let output = Command::new("ssh")
117        .args([
118            "-oStrictHostKeyChecking=no",
119            "-oPasswordAuthentication=no",
120            "-oConnectTimeout=30",
121            "-l",
122            user,
123            host,
124            &cmd,
125        ])
126        .output()
127        .map_err(|e| NixError::ActivationFailed {
128            host: host.to_string(),
129            message: format!("failed to set profile: {}", e),
130        })?;
131
132    if !output.status.success() {
133        let stderr = String::from_utf8_lossy(&output.stderr);
134        return Err(NixError::ActivationFailed {
135            host: host.to_string(),
136            message: format!("profile set failed: {}", stderr),
137        });
138    }
139
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_profile_set_only_for_switch_and_boot() {
149        // Verify all DeployAction variants are accounted for in the match.
150        let no_profile_actions = vec![
151            DeployAction::Test,
152            DeployAction::DryActivate,
153            DeployAction::Build,
154        ];
155        let profile_actions = vec![DeployAction::Switch, DeployAction::Boot];
156        assert_eq!(no_profile_actions.len() + profile_actions.len(), 5);
157    }
158
159    #[test]
160    fn test_activation_command_nixos() {
161        let toplevel = "/nix/store/abc-nixos-system";
162        let cmd = format!(
163            "{}/bin/switch-to-configuration {}",
164            toplevel,
165            DeployAction::Switch
166        );
167        assert_eq!(
168            cmd,
169            "/nix/store/abc-nixos-system/bin/switch-to-configuration switch"
170        );
171
172        let cmd = format!(
173            "{}/bin/switch-to-configuration {}",
174            toplevel,
175            DeployAction::DryActivate
176        );
177        assert_eq!(
178            cmd,
179            "/nix/store/abc-nixos-system/bin/switch-to-configuration dry-activate"
180        );
181    }
182
183    #[test]
184    fn test_activation_command_darwin() {
185        let toplevel = "/nix/store/abc-darwin-system";
186        let cmd = format!("{}/activate-user && sudo {}/activate", toplevel, toplevel);
187        assert_eq!(
188            cmd,
189            "/nix/store/abc-darwin-system/activate-user && sudo /nix/store/abc-darwin-system/activate"
190        );
191    }
192}