1use std::{2 cell::{Ref, RefCell, RefMut},3 env::current_dir,4 ffi::{OsStr, OsString},5 ops::Deref,6 path::PathBuf,7 sync::Arc,8};910use anyhow::Result;11use serde::de::DeserializeOwned;12use structopt::clap::ArgGroup;13use structopt::StructOpt;14use tokio::process::Command;1516use crate::{command::CommandExt, fleetdata::FleetData};1718pub struct FleetConfigInternals {19 pub local_system: String,20 pub directory: PathBuf,21 pub opts: FleetOpts,22 pub data: RefCell<FleetData>,23}2425#[derive(Clone)]26pub struct Config(Arc<FleetConfigInternals>);2728impl Deref for Config {29 type Target = FleetConfigInternals;3031 fn deref(&self) -> &Self::Target {32 &self.033 }34}3536impl Config {37 pub fn should_skip(&self, host: &str) -> bool {38 if !self.opts.skip.is_empty() {39 self.opts.skip.iter().any(|h| h as &str == host)40 } else if !self.opts.only.is_empty() {41 !self.opts.only.iter().any(|h| h as &str == host)42 } else {43 false44 }45 }46 pub fn is_local(&self, host: &str) -> bool {47 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)48 }4950 pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {51 if self.is_local(host) {52 if sudo {53 let mut cmd = Command::new("sudo");54 cmd.arg(program);55 cmd56 } else {57 Command::new(program)58 }59 } else {60 let mut cmd = Command::new("ssh");61 cmd.arg(host).arg("--");62 if sudo {63 cmd.arg("sudo");64 }65 cmd.arg(program);66 cmd67 }68 }6970 pub fn configuration_attr_name(&self, name: &str) -> OsString {71 let mut str = self.directory.as_os_str().to_owned();72 str.push("#");73 str.push(&format!(74 "fleetConfigurations.default.{}.{}",75 self.local_system, name76 ));77 str78 }7980 pub async fn list_hosts(&self) -> Result<Vec<String>> {81 Command::new("nix")82 .arg("eval")83 .arg(self.configuration_attr_name("configuredHosts"))84 .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])85 .run_nix_json()86 .await87 }88 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {89 Command::new("nix")90 .arg("eval")91 .arg(92 self.configuration_attr_name(&format!(93 "configuredSystems.{}.config.{}",94 host, attr95 )),96 )97 .args(&["--json", "--show-trace"])98 .run_nix_json()99 .await100 }101102 pub fn data(&self) -> Ref<FleetData> {103 self.data.borrow()104 }105 pub fn data_mut(&self) -> RefMut<FleetData> {106 self.data.borrow_mut()107 }108109 pub fn save(&self) -> Result<()> {110 let mut fleet_data_path = self.directory.clone();111 fleet_data_path.push("fleet.nix");112 let data = nixlike::serialize(&self.data() as &FleetData)?;113 std::fs::write(114 fleet_data_path,115 format!(116 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",117 data118 ),119 )?;120 Ok(())121 }122}123124#[derive(StructOpt, Clone)]125#[structopt(group = ArgGroup::with_name("target_hosts"))]126pub struct FleetOpts {127 128 #[structopt(long, number_of_values = 1, group = "target_hosts")]129 only: Vec<String>,130131 132 #[structopt(long, number_of_values = 1, group = "target_hosts")]133 skip: Vec<String>,134135 136 #[structopt(long)]137 pub localhost: Option<String>,138139 #[structopt(long, default_value = "x86_64-linux")]140 pub local_system: String,141}142143impl FleetOpts {144 pub fn build(mut self) -> Result<Config> {145 let local_system = self.local_system.clone();146 if self.localhost.is_none() {147 self.localhost148 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());149 }150 let directory = current_dir()?;151152 let mut fleet_data_path = directory.clone();153 fleet_data_path.push("fleet.nix");154 let bytes = std::fs::read_to_string(fleet_data_path)?;155 let data = nixlike::parse_str(&bytes)?;156157 Ok(Config(Arc::new(FleetConfigInternals {158 opts: self,159 directory,160 data,161 local_system,162 })))163 }164}