1use std::collections::HashMap;
4use std::process::Command;
5
6use crate::config::{DeployAction, DeploymentPlan, DeploymentTarget, FleetConfig};
7use crate::error::{NixError, Result};
8
9pub fn evaluate(
11 config: &FleetConfig,
12 target_nodes: &[String],
13 action: DeployAction,
14 max_parallel: usize,
15) -> Result<DeploymentPlan> {
16 let mut plan = DeploymentPlan::new(action, max_parallel);
17
18 for name in target_nodes {
19 let node = config
20 .nodes
21 .get(name)
22 .ok_or_else(|| NixError::General(format!("unknown node: {}", name)))?;
23
24 let toplevel_path = eval_toplevel(&config.flake_uri, name)?;
26
27 plan.targets.push(DeploymentTarget {
28 node: node.clone(),
29 toplevel_path,
30 current_system: None, needs_build: true,
32 needs_copy: true,
33 });
34 }
35
36 Ok(plan)
37}
38
39pub fn eval_toplevel(flake_uri: &str, hostname: &str) -> Result<String> {
41 let attr = format!(
42 "{}#nixosConfigurations.{}.config.system.build.toplevel.outPath",
43 flake_uri, hostname
44 );
45
46 let output = Command::new("nix")
47 .args(["eval", "--raw", &attr])
48 .output()
49 .map_err(|e| NixError::EvalFailed {
50 host: hostname.to_string(),
51 message: format!("failed to run nix eval: {}", e),
52 })?;
53
54 if !output.status.success() {
55 let stderr = String::from_utf8_lossy(&output.stderr);
56 return Err(NixError::EvalFailed {
57 host: hostname.to_string(),
58 message: stderr.to_string(),
59 });
60 }
61
62 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
63 if path.is_empty() {
64 return Err(NixError::EvalFailed {
65 host: hostname.to_string(),
66 message: "nix eval returned empty path".to_string(),
67 });
68 }
69
70 Ok(path)
71}
72
73pub fn query_current_system(host: &str, user: &str) -> Result<Option<String>> {
75 let output = Command::new("ssh")
76 .args([
77 "-oStrictHostKeyChecking=no",
78 "-oPasswordAuthentication=no",
79 "-oConnectTimeout=10",
80 "-l",
81 user,
82 host,
83 "readlink",
84 "/run/current-system",
85 ])
86 .output()
87 .map_err(|e| NixError::SshFailed {
88 host: host.to_string(),
89 message: format!("failed to query current system: {}", e),
90 })?;
91
92 if !output.status.success() {
93 return Ok(None);
94 }
95
96 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
97 if path.is_empty() {
98 Ok(None)
99 } else {
100 Ok(Some(path))
101 }
102}
103
104pub fn eval_all(flake_uri: &str, hostnames: &[String]) -> Result<HashMap<String, String>> {
106 let mut results = HashMap::new();
107 for hostname in hostnames {
109 let path = eval_toplevel(flake_uri, hostname)?;
110 results.insert(hostname.clone(), path);
111 }
112 Ok(results)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_eval_toplevel_attr_format() {
121 let flake = ".";
123 let host = "contra";
124 let attr = format!(
125 "{}#nixosConfigurations.{}.config.system.build.toplevel.outPath",
126 flake, host
127 );
128 assert_eq!(
129 attr,
130 ".#nixosConfigurations.contra.config.system.build.toplevel.outPath"
131 );
132 }
133}