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,76 name77 ));78 str79 }8081 pub async fn list_hosts(&self) -> Result<Vec<String>> {82 Command::new("nix")83 .arg("eval")84 .arg(self.configuration_attr_name("configuredHosts"))85 .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])86 .run_nix_json()87 .await88 }89 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {90 Command::new("nix")91 .arg("eval")92 .arg(self.configuration_attr_name(&format!("configuredSystems.{}.config.{}", host, attr)))93 .args(&["--json", "--show-trace"])94 .run_nix_json()95 .await96 }9798 pub fn data(&self) -> Ref<FleetData> {99 self.data.borrow()100 }101 pub fn data_mut(&self) -> RefMut<FleetData> {102 self.data.borrow_mut()103 }104105 pub fn save(&self) -> Result<()> {106 let mut fleet_data_path = self.directory.clone();107 fleet_data_path.push("fleet.nix");108 let data = nixlike::serialize(&self.data() as &FleetData)?;109 std::fs::write(110 fleet_data_path,111 format!(112 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",113 data114 ),115 )?;116 Ok(())117 }118}119120#[derive(StructOpt, Clone)]121#[structopt(group = ArgGroup::with_name("target_hosts"))]122pub struct FleetOpts {123 124 #[structopt(long, number_of_values = 1, group = "target_hosts")]125 only: Vec<String>,126127 128 #[structopt(long, number_of_values = 1, group = "target_hosts")]129 skip: Vec<String>,130131 132 #[structopt(long)]133 pub localhost: Option<String>,134135 #[structopt(long, default_value = "x86_64-linux")]136 pub local_system: String,137}138139impl FleetOpts {140 pub fn build(mut self) -> Result<Config> {141 let local_system = self.local_system.clone();142 if self.localhost.is_none() {143 self.localhost144 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());145 }146 let directory = current_dir()?;147148 let mut fleet_data_path = directory.clone();149 fleet_data_path.push("fleet.nix");150 let bytes = std::fs::read_to_string(fleet_data_path)?;151 let data = nixlike::parse_str(&bytes)?;152153 Ok(Config(Arc::new(FleetConfigInternals {154 opts: self,155 directory,156 data,157 local_system,158 })))159 }160}