git.delta.rocks / jrsonnet / refs/commits / 7c6930a6bff0

difftreelog

source

cmds/fleet/src/host.rs10.2 KiBsourcehistory
1use std::{2	env::current_dir,3	ffi::{OsStr, OsString},4	fmt::Display,5	io::Write,6	ops::Deref,7	path::PathBuf,8	str::FromStr,9	sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{anyhow, bail, Context, Result};13use clap::{ArgGroup, Parser};14use openssh::SessionBuilder;15use serde::de::DeserializeOwned;16use tempfile::NamedTempFile;1718use crate::{19	better_nix_eval::{Field, NixSessionPool},20	command::MyCommand,21	fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},22	nix_go, nix_go_json,23};2425pub struct FleetConfigInternals {26	pub local_system: String,27	pub directory: PathBuf,28	pub opts: FleetOpts,29	pub data: Mutex<FleetData>,30	pub nix_args: Vec<OsString>,31	/// fleetConfigurations.<name>.<localSystem>32	pub fleet_field: Field,33	/// fleet_config.configUnchecked34	pub config_field: Field,35	/// fleet_config.unchecked36	pub config_unchecked_field: Field,37}3839#[derive(Clone)]40pub struct Config(Arc<FleetConfigInternals>);4142impl Deref for Config {43	type Target = FleetConfigInternals;4445	fn deref(&self) -> &Self::Target {46		&self.047	}48}4950pub struct ConfigHost {51	pub name: String,52	pub local: bool,53	pub session: OnceLock<Arc<openssh::Session>>,54}55impl ConfigHost {56	async fn open_session(&self) -> Result<Arc<openssh::Session>> {57		assert!(!self.local, "do not open ssh connection to local session");58		// FIXME: TOCTOU59		if let Some(session) = &self.session.get() {60			return Ok((*session).clone());61		};62		let session = SessionBuilder::default();6364		let session = session65			.connect(&self.name)66			.await67			.map_err(|e| anyhow!("ssh error: {e}"))?;68		let session = Arc::new(session);69		self.session.set(session.clone()).expect("TOCTOU happened");70		Ok(session)71	}72	pub async fn mktemp_dir(&self) -> Result<String> {73		let mut cmd = self.cmd("mktemp").await?;74		cmd.arg("-d");75		let path = cmd.run_string().await?;76		Ok(path.trim_end().to_owned())77	}78	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {79		let mut cmd = self.cmd("cat").await?;80		cmd.arg(path);81		cmd.run_bytes().await82	}83	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {84		let mut cmd = self.cmd("cat").await?;85		cmd.arg(path);86		cmd.run_string().await87	}88	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {89		let text = self.read_file_text(path).await?;90		Ok(serde_json::from_str(&text)?)91	}92	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>93	where94		<D as FromStr>::Err: Display,95	{96		let text = self.read_file_text(path).await?;97		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))98	}99	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {100		if self.local {101			Ok(MyCommand::new(cmd))102		} else {103			let session = self.open_session().await?;104			Ok(MyCommand::new_on(cmd, session))105		}106	}107108	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {109		let mut cmd = self.cmd("fleet-install-secrets").await?;110		cmd.arg("decrypt").eqarg("--secret", data.encode_z85());111		let encoded = cmd112			.sudo()113			.run_string()114			.await115			.context("failed to call remote host for decrypt")?;116		z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")117	}118	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {119		let mut cmd = self.cmd("fleet-install-secrets").await?;120		cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());121		for target in targets {122			cmd.eqarg("--targets", target);123		}124		let encoded = cmd125			.sudo()126			.run_string()127			.await128			.context("failed to call remote host for decrypt")?;129		SecretData::decode_z85(encoded.trim_end()).context("bad encoded data? outdated host?")130	}131	/// Returns path for futureproofing, as path might change i.e on conversion to CA132	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {133		if self.local {134			// Path is located locally, thus already trusted.135			return Ok(path.to_owned());136		}137		let mut nix = MyCommand::new("nix");138		nix.arg("copy")139			.arg("--substitute-on-destination")140			.comparg("--to", format!("ssh-ng://{}", self.name))141			.arg(path);142		nix.run_nix().await?;143		Ok(path.to_owned())144	}145	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {146		let mut cmd = self.cmd("systemctl").await?;147		cmd.arg("stop").arg(name);148		cmd.sudo().run().await149	}150	pub async fn systemctl_start(&self, name: &str) -> Result<()> {151		let mut cmd = self.cmd("systemctl").await?;152		cmd.arg("start").arg(name);153		cmd.sudo().run().await154	}155156	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {157		let mut cmd = self.cmd("rm").await?;158		cmd.arg("-f").arg(path);159		if sudo {160			cmd = cmd.sudo()161		}162		cmd.run().await163	}164}165166impl Config {167	pub fn should_skip(&self, host: &str) -> bool {168		if !self.opts.skip.is_empty() {169			self.opts.skip.iter().any(|h| h as &str == host)170		} else if !self.opts.only.is_empty() {171			!self.opts.only.iter().any(|h| h as &str == host)172		} else {173			false174		}175	}176	pub fn is_local(&self, host: &str) -> bool {177		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)178	}179180	pub async fn host(&self, name: &str) -> Result<ConfigHost> {181		Ok(ConfigHost {182			name: name.to_owned(),183			local: self.is_local(name),184			session: OnceLock::new(),185		})186	}187	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {188		let fleet_field = &self.fleet_field;189		let names = nix_go!(fleet_field.configuredHosts).list_fields().await?;190		let mut out = vec![];191		for name in names {192			out.push(ConfigHost {193				local: self.is_local(&name),194				name,195				session: OnceLock::new(),196			})197		}198		Ok(out)199	}200	pub async fn system_config(&self, host: &str) -> Result<Field> {201		let fleet_field = &self.fleet_field;202		Ok(nix_go!(fleet_field.configuredSystems[{ host }].config))203	}204205	pub(super) fn data(&self) -> MutexGuard<FleetData> {206		self.data.lock().unwrap()207	}208	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {209		self.data.lock().unwrap()210	}211	/// Shared secrets configured in fleet.nix or in flake212	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {213		let config_field = &self.config_unchecked_field;214		nix_go!(config_field.configUnchecked.sharedSecrets)215			.list_fields()216			.await217	}218	/// Shared secrets configured in fleet.nix219	pub fn list_shared(&self) -> Vec<String> {220		let data = self.data();221		data.shared_secrets.keys().cloned().collect()222	}223	pub fn has_shared(&self, name: &str) -> bool {224		let data = self.data();225		data.shared_secrets.contains_key(name)226	}227	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {228		let mut data = self.data_mut();229		data.shared_secrets.insert(name.to_owned(), shared);230	}231	pub fn remove_shared(&self, secret: &str) {232		let mut data = self.data_mut();233		data.shared_secrets.remove(secret);234	}235236	pub fn has_secret(&self, host: &str, secret: &str) -> bool {237		let data = self.data();238		let Some(host_secrets) = data.host_secrets.get(host) else {239			return false;240		};241		host_secrets.contains_key(secret)242	}243	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {244		let mut data = self.data_mut();245		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();246		host_secrets.insert(secret, value);247	}248249	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {250		let data = self.data();251		let Some(host_secrets) = data.host_secrets.get(host) else {252			bail!("no secrets for machine {host}");253		};254		let Some(secret) = host_secrets.get(secret) else {255			bail!("machine {host} has no secret {secret}");256		};257		Ok(secret.clone())258	}259	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {260		let data = self.data();261		let Some(secret) = data.shared_secrets.get(secret) else {262			bail!("no shared secret {secret}");263		};264		Ok(secret.clone())265	}266	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {267		let config_field = &self.config_unchecked_field;268		Ok(nix_go_json!(269			config_field.configUnchecked.sharedSecrets[{ secret }].expectedOwners270		))271	}272273	pub fn save(&self) -> Result<()> {274		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;275		let data = nixlike::serialize(&self.data() as &FleetData)?;276		tempfile.write_all(277			format!(278				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",279				data280			)281			.as_bytes(),282		)?;283		let mut fleet_data_path = self.directory.clone();284		fleet_data_path.push("fleet.nix");285		tempfile.persist(fleet_data_path)?;286		Ok(())287	}288}289290#[derive(Parser, Clone)]291#[clap(group = ArgGroup::new("target_hosts"))]292pub struct FleetOpts {293	/// All hosts except those would be skipped294	#[clap(long, number_of_values = 1, group = "target_hosts")]295	only: Vec<String>,296297	/// Hosts to skip298	#[clap(long, number_of_values = 1, group = "target_hosts")]299	skip: Vec<String>,300301	/// Host, which should be threaten as current machine302	#[clap(long)]303	pub localhost: Option<String>,304305	/// Override detected system for host, to perform builds via306	/// binfmt-declared qemu instead of trying to crosscompile307	#[clap(long, default_value = "detect")]308	pub local_system: String,309}310311impl FleetOpts {312	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {313		if self.localhost.is_none() {314			self.localhost315				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());316		}317		let directory = current_dir()?;318319		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;320		let root_field = pool.get().await?;321322		if self.local_system == "detect" {323			let builtins_field = Field::field(root_field.clone(), "builtins").await?;324			self.local_system = nix_go_json!(builtins_field.currentSystem);325		}326		let local_system = self.local_system.clone();327328		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;329330		let fleet_field = nix_go!(fleet_root.default);331		let config_field = nix_go!(fleet_field.configUnchecked);332		let config_unchecked_field = nix_go!(fleet_field.unchecked);333334		let mut fleet_data_path = directory.clone();335		fleet_data_path.push("fleet.nix");336		let bytes = std::fs::read_to_string(fleet_data_path)?;337		let data = nixlike::parse_str(&bytes)?;338339		Ok(Config(Arc::new(FleetConfigInternals {340			opts: self,341			directory,342			data,343			local_system,344			nix_args,345			fleet_field,346			config_field,347			config_unchecked_field,348		})))349	}350}