difftreelog
feat fleetdata
in: trunk
9 files changed
src/cmds/build_systems.rsdiffbeforeafterboth1use std::process::Command;1use std::process::Command;223use crate::{3use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE};4 command::CommandExt,5 db::{secret::SecretDb, Db, DbData},6 host::FleetOpts,7 nix::SYSTEMS_ATTRIBUTE,8};9use anyhow::Result;4use anyhow::Result;10use clap::Clap;5use clap::Clap;11use log::{info, warn};6use log::info;12713#[derive(Clap)]8#[derive(Clap)]14#[clap(group = clap::ArgGroup::new("target"))]9#[clap(group = clap::ArgGroup::new("target"))]15pub struct BuildSystems {10pub struct BuildSystems {16 #[clap(flatten)]17 fleet_opts: FleetOpts,18 /// --builders arg for nix11 /// --builders arg for nix19 #[clap(long)]12 #[clap(long)]20 builders: Option<String>,13 builders: Option<String>,53}46}544755impl BuildSystems {48impl BuildSystems {56 pub fn run(self) -> Result<()> {49 pub fn run(self, config: &Config) -> Result<()> {57 let fleet = self.fleet_opts.build()?;58 let db = Db::new(".fleet")?;50 println!("Build");51 // let db = Db::new(".fleet")?;59 let hosts = fleet.list_hosts()?;52 let hosts = config.list_hosts()?;60 let data = SecretDb::open(&db)?.generate_nix_data()?;53 dbg!(&hosts);54 // let data = SecretDb::open(&db)?.generate_nix_data()?;615562 for host in hosts.iter() {56 for host in hosts.iter() {63 if host.skip() {57 if config.should_skip(host) {64 warn!("Skipping host {}", host.hostname);65 continue;58 continue;66 }59 }67 info!("Building host {}", host.hostname);60 info!("Building host {}", host);68 let built = {61 let built = {69 let dir = tempfile::tempdir()?;62 let dir = tempfile::tempdir()?;70 dir.path().to_owned()63 dir.path().to_owned()82 .arg(&built)75 .arg(&built)83 .arg(format!(76 .arg(format!(84 "{}.{}.config.system.build.toplevel",77 "{}.{}.config.system.build.toplevel",85 SYSTEMS_ATTRIBUTE, host.hostname,78 SYSTEMS_ATTRIBUTE, host,86 ))79 ));87 .env("SECRET_DATA", data.clone());80 // .env("SECRET_DATA", data.clone());888189 if let Some(builders) = &self.builders {82 if let Some(builders) = &self.builders {90 println!("Using builders: {}", builders);83 println!("Using builders: {}", builders);101 nix_build.inherit_stdio().run()?;94 nix_build.inherit_stdio().run()?;102 let built = std::fs::canonicalize(built)?;95 let built = std::fs::canonicalize(built)?;103 info!("Built closure: {:?}", built);96 info!("Built closure: {:?}", built);104 if !host.is_local() {97 if !config.is_local(host) {105 info!("Uploading system closure");98 info!("Uploading system closure");106 Command::new("nix")99 Command::new("nix")107 .args(&["copy", "--to"])100 .args(&["copy", "--to"])108 .arg(format!("ssh://root@{}", host.hostname))101 .arg(format!("ssh://root@{}", host))109 .arg(&built)102 .arg(&built)110 .inherit_stdio()103 .inherit_stdio()111 .run()?;104 .run()?;112 }105 }113 if let Some(subcommand) = &self.subcommand {106 if let Some(subcommand) = &self.subcommand {114 if subcommand.should_switch_profile() {107 if subcommand.should_switch_profile() {115 info!("Switching generation");108 info!("Switching generation");109 dbg!(&mut config116 host.command_on("nix-env", true)110 .command_on(host, "nix-env", true)117 .args(&["-p", "/nix/var/nix/profiles/system", "--set"])111 .args(&["-p", "/nix/var/nix/profiles/system", "--set"])118 .arg(&built)112 .arg(&built)119 .inherit_stdio()113 .inherit_stdio())120 .run()?;114 .run()?;121 }115 }122 info!("Executing activation script");116 info!("Executing activation script");123 let mut switch_script = built.clone();117 let mut switch_script = built.clone();124 switch_script.push("bin");118 switch_script.push("bin");125 switch_script.push("switch-to-configuration");119 switch_script.push("switch-to-configuration");126 info!("{:?}", switch_script);120 info!("{:?}", switch_script);121 config127 host.command_on(switch_script, true)122 .command_on(host, switch_script, true)128 .arg(subcommand.name())123 .arg(subcommand.name())129 .inherit_stdio()124 .inherit_stdio()130 .run()?;125 .run()?;src/cmds/generate_secrets.rsdiffbeforeafterboth4use clap::Clap;4use clap::Clap;5use log::info;5use log::info;667use crate::db::{7use crate::{8 db::{8 secret::{list_secrets, SecretDb},9 secret::{list_secrets, SecretDb},9 Db, DbData,10 Db, DbData,10};11 },12 host::FleetOpts,13};111412#[derive(Clap)]15#[derive(Clap)]13pub struct GenerateSecrets {16pub struct GenerateSecrets {17 #[clap(flatten)]18 fleet_opts: FleetOpts,1914 /// If set - remove orphaned secrets20 /// If set - remove orphaned secrets15 #[clap(long)]21 #[clap(long)]232924 let defined_secrets = list_secrets()?;30 let defined_secrets = list_secrets()?;25 for (secret, data) in defined_secrets.iter() {31 for (secret, data) in defined_secrets.iter() {26 // let keys = KeyDb::open(&db)?;32 //let keys = KeyDb::open(&db)?;27 // secrets.ensure_generated(&keys, &secret, &data)?;33 secrets.ensure_generated(&self.fleet_opts, secret, data)?;28 }34 }29 let key_names = defined_secrets35 let key_names = defined_secrets30 .keys()36 .keys()src/cmds/mod.rsdiffbeforeafterboth1pub mod build_systems;1pub mod build_systems;2// pub mod fetch_keys;2pub mod fetch_keys;3pub mod generate_secrets;3pub mod generate_secrets;4pub mod secrets;45src/command.rsdiffbeforeafterboth23 fn run(&mut self) -> Result<()> {23 fn run(&mut self) -> Result<()> {24 let out = self.output()?;24 let out = self.output()?;25 if !out.status.success() {25 if !out.status.success() {26 anyhow::bail!("command failed");26 anyhow::bail!("command failed with status {}", out.status);27 }27 }28 Ok(())28 Ok(())29 }29 }303031 fn run_json<T: DeserializeOwned>(&mut self) -> Result<T> {31 fn run_json<T: DeserializeOwned>(&mut self) -> Result<T> {32 let str = self.run_string()?;32 let str = self.run_string()?;33 Ok(serde_json::from_str(&str).with_context(|| format!("{:?}", str))?)33 serde_json::from_str(&str).with_context(|| format!("{:?}", str))34 }34 }353536 fn run_string(&mut self) -> Result<String> {36 fn run_string(&mut self) -> Result<String> {src/fleetdata.rsdiffbeforeafterbothno changes
src/host.rsdiffbeforeafterboth1use std::{1use std::{2 cell::{Ref, RefCell, RefMut},2 env::current_dir,3 env::current_dir,3 ffi::{OsStr, OsString},4 ffi::{OsStr, OsString},4 ops::Deref,5 ops::Deref,10use anyhow::Result;11use anyhow::Result;11use clap::Clap;12use clap::Clap;121313use crate::command::CommandExt;14use crate::{command::CommandExt, fleetdata::FleetData};141515pub struct FleetConfigInternals {16pub struct FleetConfigInternals {16 pub directory: PathBuf,17 pub directory: PathBuf,17 pub opts: FleetOpts,18 pub opts: FleetOpts,19 pub data: RefCell<FleetData>,18}20}192120#[derive(Clone)]22#[derive(Clone)]21pub struct FleetConfig(Arc<FleetConfigInternals>);23pub struct Config(Arc<FleetConfigInternals>);222423impl Deref for FleetConfig {25impl Deref for Config {24 type Target = FleetConfigInternals;26 type Target = FleetConfigInternals;252726 fn deref(&self) -> &Self::Target {28 fn deref(&self) -> &Self::Target {27 &self.029 &self.028 }30 }29}31}303231impl FleetConfig {33impl Config {34 pub fn should_skip(&self, host: &str) -> bool {35 if !self.opts.skip.is_empty() {36 self.opts.skip.iter().any(|h| h as &str == host)37 } else if !self.opts.only.is_empty() {38 !self.opts.only.iter().any(|h| h as &str == host)39 } else {40 false41 }42 }43 pub fn is_local(&self, host: &str) -> bool {44 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)45 }4632 pub fn data_dir(&self) -> PathBuf {47 pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {48 if self.is_local(host) {49 if sudo {33 let mut out = self.directory.clone();50 let mut cmd = Command::new("sudo");51 cmd.arg(program);52 cmd53 } else {54 Command::new(program)55 }56 } else {57 let mut cmd = Command::new("ssh");34 out.push(".fleet");58 cmd.arg(host).arg("--");35 out59 if sudo {60 cmd.arg("sudo");61 }62 cmd.arg(program);63 cmd64 }36 }65 }376638 pub fn full_attr_name(&self, attr_name: &str) -> OsString {67 pub fn full_attr_name(&self, attr_name: &str) -> OsString {39 let mut str = self.directory.as_os_str().to_owned();68 let mut str = self.directory.as_os_str().to_owned();40 str.push("#");69 str.push("#");41 str.push(attr_name);70 str.push(attr_name);7172 println!("{:?}", str);42 str73 str43 }74 }447545 pub fn list_host_names(&self) -> Result<Vec<String>> {76 pub fn list_hosts(&self) -> Result<Vec<String>> {46 Ok(Command::new("nix")77 Command::new("nix")47 .arg("eval")78 .arg("eval")48 .arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))79 .arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))49 .args(&["--apply", "builtins.attrNames", "--json"])80 .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])50 .inherit_stdio()51 .run_json()?)81 .inherit_stdio()82 .run_json()52 }83 }8485 pub fn data(&self) -> Ref<FleetData> {86 self.data.borrow()87 }88 pub fn data_mut(&self) -> RefMut<FleetData> {89 self.data.borrow_mut()90 }539154 pub fn list_hosts(&self) -> Result<Vec<Host>> {92 pub fn save(&self) -> Result<()> {55 Ok(self93 let mut fleet_data_path = self.directory.clone();56 .list_host_names()?94 fleet_data_path.push("fleet.nix");57 .into_iter()58 .map(|hostname| Host {95 let data = nixlike::serialize(&self.data() as &FleetData)?;59 fleet_config: self.clone(),96 std::fs::write(fleet_data_path, data)?;60 hostname,97 Ok(())61 })62 .collect())63 }98 }64}99}6566pub struct Host {67 pub fleet_config: FleetConfig,6869 pub hostname: String,70}7172impl Host {73 pub fn skip(&self) -> bool {74 self.fleet_config.0.opts.should_skip(&self.hostname)75 }76 pub fn is_local(&self) -> bool {77 self.fleet_config.0.opts.is_local(&self.hostname)78 }79 pub fn command_on(&self, cmd: impl AsRef<OsStr>, sudo: bool) -> Command {80 if !self.is_local() {81 let mut out = Command::new("ssh");82 out.arg(&self.hostname).arg("--");83 if sudo {84 out.arg("sudo");85 }86 out.arg(cmd);87 out88 } else if sudo {89 let mut out = Command::new("sudo");90 out.arg(cmd);91 out92 } else {93 Command::new(cmd)94 }95 }96}9710098#[derive(Clap, Clone)]101#[derive(Clap, Clone)]99#[clap(group = clap::ArgGroup::new("target_hosts"))]102#[clap(group = clap::ArgGroup::new("target_hosts"))]112}115}113116114impl FleetOpts {117impl FleetOpts {115 pub fn should_skip(&self, host: &str) -> bool {116 if self.skip.len() > 0 {117 self.skip.iter().find(|h| h as &str == host).is_some()118 } else if self.only.len() > 0 {119 self.only.iter().find(|h| h as &str == host).is_none()120 } else {121 false122 }123 }124 pub fn is_local(&self, host: &str) -> bool {125 self.localhost.as_ref().map(|s| &s as &str) == Some(host)126 }127 pub fn build(mut self) -> Result<FleetConfig> {118 pub fn build(mut self) -> Result<Config> {128 if self.localhost.is_none() {119 if self.localhost.is_none() {129 self.localhost120 self.localhost130 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());121 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());131 }122 }132 let directory = current_dir()?;123 let directory = current_dir()?;124125 let mut fleet_data_path = directory.clone();126 fleet_data_path.push("fleet.nix");127 let bytes = std::fs::read_to_string(fleet_data_path)?;128 let data = nixlike::parse_str(&bytes)?;129133 Ok(FleetConfig(Arc::new(FleetConfigInternals {130 Ok(Config(Arc::new(FleetConfigInternals {134 opts: self,131 opts: self,135 directory,132 directory,133 data,136 })))134 })))137 }135 }138}136}src/keys.rsdiffbeforeafterboth1use std::str::FromStr;21use crate::{3use crate::{command::CommandExt, host::Config};2 command::CommandExt,3 host::{FleetConfig, Host},4};5use anyhow::Result;4use anyhow::{anyhow, Result};6use log::warn;5use log::warn;7use std::{8 fs::{create_dir_all, metadata, read, read_dir, write},9 path::PathBuf,10};1112impl FleetConfig {13 fn host_keys_dir(&self) -> Result<PathBuf> {14 let mut out = self.data_dir().clone();15 out.push("host_keys");16 create_dir_all(&out)?;17 Ok(out)18 }1920 fn host_key_file(&self, host: &str) -> Result<PathBuf> {21 let mut dir = self.host_keys_dir()?;22 dir.push(format!("{}.asc", host));23 Ok(dir)24 }2526 pub fn list_orphaned_keys(&self) -> Result<Vec<(String, PathBuf)>> {27 let mut out = Vec::new();28 let host_names = self.list_host_names()?;29 for file in read_dir(&self.host_keys_dir()?)? {30 let file = file?;31 anyhow::ensure!(32 file.file_type()?.is_file(),33 "host_keys dir should contain only files"34 );35 let name = file.file_name();36 let name = name.to_str().unwrap();37 if let Some(hostname) = name.strip_suffix(".asc") {38 if !host_names.contains(&hostname.to_owned()) {39 out.push((hostname.to_owned(), file.path()))40 }41 } else {42 out.push(("<unknown>".to_owned(), file.path()))43 }44 }4546 Ok(out)47 }48}49650impl Host {7impl Config {51 pub fn key(&self) -> anyhow::Result<String> {8 pub fn cached_key(&self, host: &str) -> Option<String> {9 let data = self.data();10 let key = data.hosts.get(host).map(|h| &h.encryption_key);11 if let Some(key) = key {12 if key.is_empty() {13 return None;14 }15 }16 key.cloned()17 }18 pub fn update_key(&self, host: &str, key: String) {19 let mut data = self.data_mut();52 let key_path = self.fleet_config.host_key_file(&self.hostname)?;20 let host = data.hosts.entry(host.to_string()).or_default();21 host.encryption_key = key.trim().to_string();22 }53 if metadata(&key_path).map(|m| m.is_file()).unwrap_or(false) {23 pub fn update_secret(&self, host: &str, name: &str, value: &[u8]) {24 let mut data = self.data_mut();25 let host = data.hosts.entry(host.to_string()).or_default();26 host.encrypted_secrets.insert(27 name.to_string(),28 format!("[ENCRYPTED:{}]", base64::encode(value)),29 );30 }3132 pub fn key(&self, host: &str) -> anyhow::Result<String> {33 if let Some(key) = self.cached_key(host) {54 Ok(String::from_utf8(read(key_path)?)?)34 Ok(key)55 } else {35 } else {56 warn!("Loading key for {}", self.hostname);36 warn!("Loading key for {}", host);57 let key = self37 let key = self58 .command_on("cat", false)38 .command_on("host", "cat", false)59 .arg("/etc/ssh/ssh_host_ed25519_key.pub")39 .arg("/etc/ssh/ssh_host_ed25519_key.pub")60 .run_string()?;40 .run_string()?;61 write(key_path, key.clone())?;41 self.update_key(host, key.clone());62 Ok(key)42 Ok(key)63 }43 }64 }44 }45 pub fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {46 let key = self.key(host)?;47 age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))48 }4950 pub fn orphaned_data(&self) -> Result<Vec<String>> {51 let mut out = Vec::new();52 let host_names = self.list_hosts()?;53 for hostname in self54 .data()55 .hosts56 .iter()57 .filter(|(_, host)| !host.encryption_key.is_empty())58 .map(|(n, _)| n)59 {60 if !host_names.contains(&hostname.to_owned()) {61 out.push(hostname.to_owned())62 }63 }6465 Ok(out)66 }65}67}6668src/main.rsdiffbeforeafterboth8pub mod db;8pub mod db;9pub mod nix;9pub mod nix;1011mod fleetdata;101211use anyhow::Result;13use anyhow::Result;12use clap::Clap;14use clap::Clap;1513use cmds::{build_systems::BuildSystems, fetch_keys::FetchKeys, generate_secrets::GenerateSecrets};16use cmds::{build_systems::BuildSystems, generate_secrets::GenerateSecrets, secrets::Secrets};17use host::{Config, FleetOpts};141815#[derive(Clap)]19#[derive(Clap)]16#[clap(version = "1.0", author = "CertainLach <iam@lach.pw>")]20#[clap(version = "1.0", author = "CertainLach <iam@lach.pw>")]23 Secrets(Secrets),27 Secrets(Secrets),24}28}2930#[derive(Clap)]31struct RootOpts {32 #[clap(flatten)]33 fleet_opts: FleetOpts,34 #[clap(subcommand)]35 command: Opts,36}3738fn run_command(config: &Config, command: Opts) -> Result<()> {39 match command {40 Opts::BuildSystems(c) => c.run(config)?,41 Opts::GenerateSecrets(c) => c.run()?,42 Opts::Secrets(s) => s.run(config)?,43 };44 Ok(())45}254626fn main() -> Result<()> {47fn main() -> Result<()> {27 env_logger::Builder::new()48 env_logger::Builder::new()28 .filter_level(log::LevelFilter::Info)49 .filter_level(log::LevelFilter::Info)29 .init();50 .init();30 let opts = Opts::parse();51 let opts = RootOpts::parse();52 let config = opts.fleet_opts.build()?;315332 match opts {54 match run_command(&config, opts.command) {33 Opts::FetchKeys(c) => c.run()?,34 Opts::BuildSystems(c) => c.run()?,55 Ok(()) => {56 config.save()?;57 Ok(())58 }35 Opts::GenerateSecrets(c) => c.run()?,59 Err(e) => {36 };60 let _ = config.save();37 Ok(())61 Err(e)62 }63 }38}64}3965src/nixlike.rsdiffbeforeafterbothno syntactic changes