git.delta.rocks / jrsonnet / refs/commits / d8c8e6d33ef5

difftreelog

source

cmds/fleet/src/host.rs11.9 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	/// fleet_config.config32	pub config_field: Field,33	/// fleet_config.unchecked.config34	pub config_unchecked_field: Field,3536	/// import nixpkgs {system = local};37	pub default_pkgs: Field,38}3940#[derive(Clone)]41pub struct Config(Arc<FleetConfigInternals>);4243impl Deref for Config {44	type Target = FleetConfigInternals;4546	fn deref(&self) -> &Self::Target {47		&self.048	}49}5051pub struct ConfigHost {52	config: Config,53	pub name: String,54	pub local: bool,55	pub session: OnceLock<Arc<openssh::Session>>,5657	pub nixos_config: Option<Field>,58}59impl ConfigHost {60	async fn open_session(&self) -> Result<Arc<openssh::Session>> {61		assert!(!self.local, "do not open ssh connection to local session");62		// FIXME: TOCTOU63		if let Some(session) = &self.session.get() {64			return Ok((*session).clone());65		};66		let session = SessionBuilder::default();6768		let session = session69			.connect(&self.name)70			.await71			.map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;72		let session = Arc::new(session);73		self.session.set(session.clone()).expect("TOCTOU happened");74		Ok(session)75	}76	pub async fn mktemp_dir(&self) -> Result<String> {77		let mut cmd = self.cmd("mktemp").await?;78		cmd.arg("-d");79		let path = cmd.run_string().await?;80		Ok(path.trim_end().to_owned())81	}82	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {83		let mut cmd = self.cmd("cat").await?;84		cmd.arg(path);85		cmd.run_bytes().await86	}87	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {88		let mut cmd = self.cmd("cat").await?;89		cmd.arg(path);90		cmd.run_string().await91	}92	#[allow(dead_code)]93	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {94		let text = self.read_file_text(path).await?;95		Ok(serde_json::from_str(&text)?)96	}97	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>98	where99		<D as FromStr>::Err: Display,100	{101		let text = self.read_file_text(path).await?;102		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))103	}104	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {105		if self.local {106			Ok(MyCommand::new(cmd))107		} else {108			let session = self.open_session().await?;109			Ok(MyCommand::new_on(cmd, session))110		}111	}112113	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {114		let mut cmd = self.cmd("fleet-install-secrets").await?;115		cmd.arg("decrypt").eqarg("--secret", data.encode_z85());116		let encoded = cmd117			.sudo()118			.run_string()119			.await120			.context("failed to call remote host for decrypt")?;121		z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")122	}123	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {124		let mut cmd = self.cmd("fleet-install-secrets").await?;125		cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());126		for target in targets {127			let key = self.config.key(&target).await?;128			cmd.eqarg("--targets", key);129		}130		let encoded = cmd131			.sudo()132			.run_string()133			.await134			.context("failed to call remote host for decrypt")?;135		SecretData::decode_z85(encoded.trim_end()).context("bad encoded data? outdated host?")136	}137	/// Returns path for futureproofing, as path might change i.e on conversion to CA138	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {139		if self.local {140			// Path is located locally, thus already trusted.141			return Ok(path.to_owned());142		}143		let mut nix = MyCommand::new("nix");144		nix.arg("copy")145			.arg("--substitute-on-destination")146			.comparg("--to", format!("ssh-ng://{}", self.name))147			.arg(path);148		nix.run_nix().await.context("nix copy")?;149		Ok(path.to_owned())150	}151	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {152		let mut cmd = self.cmd("systemctl").await?;153		cmd.arg("stop").arg(name);154		cmd.sudo().run().await155	}156	pub async fn systemctl_start(&self, name: &str) -> Result<()> {157		let mut cmd = self.cmd("systemctl").await?;158		cmd.arg("start").arg(name);159		cmd.sudo().run().await160	}161162	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {163		let mut cmd = self.cmd("rm").await?;164		cmd.arg("-f").arg(path);165		if sudo {166			cmd = cmd.sudo()167		}168		cmd.run().await169	}170171	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {172		let Some(nixos) = &self.nixos_config else {173			return Ok(vec![]);174		};175		let secrets = nix_go!(nixos.secrets);176		let mut out = Vec::new();177		for name in secrets.list_fields().await? {178			let secret = nix_go!(secrets[{ name }]);179			let is_shared: bool = nix_go_json!(secret.shared);180			if is_shared {181				continue;182			}183			out.push(name);184		}185		Ok(out)186	}187	pub async fn secret_field(&self, name: &str) -> Result<Field> {188		let Some(nixos) = &self.nixos_config else {189			bail!("host is virtual and has no secrets");190		};191		Ok(nix_go!(nixos.secrets[{ name }]))192	}193194	/// Packages for this host, resolved with nixpkgs overlays195	pub async fn pkgs(&self) -> Result<Field> {196		let Some(nixos) = &self.nixos_config else {197			return Ok(self.config.default_pkgs.clone());198		};199		Ok(nix_go!(nixos.nixpkgs.resolvedPkgs))200	}201}202203impl Config {204	pub fn should_skip(&self, host: &str) -> bool {205		if !self.opts.skip.is_empty() {206			self.opts.skip.iter().any(|h| h as &str == host)207		} else if !self.opts.only.is_empty() {208			!self.opts.only.iter().any(|h| h as &str == host)209		} else {210			false211		}212	}213	pub fn is_local(&self, host: &str) -> bool {214		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)215	}216217	pub fn local_host(&self) -> ConfigHost {218		ConfigHost {219			config: self.clone(),220			name: "<virtual localhost>".to_owned(),221			local: true,222			session: OnceLock::new(),223			nixos_config: None,224		}225	}226227	pub async fn host(&self, name: &str) -> Result<ConfigHost> {228		let config = &self.config_unchecked_field;229		let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);230		Ok(ConfigHost {231			config: self.clone(),232			name: name.to_owned(),233			local: self.is_local(name),234			session: OnceLock::new(),235			nixos_config: Some(nixos_config),236		})237	}238	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {239		let config = &self.config_unchecked_field;240		let names = nix_go!(config.hosts).list_fields().await?;241		let mut out = vec![];242		for name in names {243			out.push(self.host(&name).await?);244		}245		Ok(out)246	}247	pub async fn system_config(&self, host: &str) -> Result<Field> {248		let fleet_field = &self.config_unchecked_field;249		Ok(nix_go!(fleet_field.hosts[{ host }].nixosSystem.config))250	}251252	pub(super) fn data(&self) -> MutexGuard<FleetData> {253		self.data.lock().unwrap()254	}255	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {256		self.data.lock().unwrap()257	}258	/// Shared secrets configured in fleet.nix or in flake259	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {260		let config_field = &self.config_unchecked_field;261		nix_go!(config_field.sharedSecrets).list_fields().await262	}263	/// Shared secrets configured in fleet.nix264	pub fn list_shared(&self) -> Vec<String> {265		let data = self.data();266		data.shared_secrets.keys().cloned().collect()267	}268	pub fn has_shared(&self, name: &str) -> bool {269		let data = self.data();270		data.shared_secrets.contains_key(name)271	}272	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {273		let mut data = self.data_mut();274		data.shared_secrets.insert(name.to_owned(), shared);275	}276	pub fn remove_shared(&self, secret: &str) {277		let mut data = self.data_mut();278		data.shared_secrets.remove(secret);279	}280281	pub fn list_secrets(&self, host: &str) -> Vec<String> {282		let data = self.data();283		let Some(secrets) = data.host_secrets.get(host) else {284			return Vec::new();285		};286		secrets.keys().cloned().collect()287	}288289	pub fn has_secret(&self, host: &str, secret: &str) -> bool {290		let data = self.data();291		let Some(host_secrets) = data.host_secrets.get(host) else {292			return false;293		};294		host_secrets.contains_key(secret)295	}296	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {297		let mut data = self.data_mut();298		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();299		host_secrets.insert(secret, value);300	}301302	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {303		let data = self.data();304		let Some(host_secrets) = data.host_secrets.get(host) else {305			bail!("no secrets for machine {host}");306		};307		let Some(secret) = host_secrets.get(secret) else {308			bail!("machine {host} has no secret {secret}");309		};310		Ok(secret.clone())311	}312	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {313		let data = self.data();314		let Some(secret) = data.shared_secrets.get(secret) else {315			bail!("no shared secret {secret}");316		};317		Ok(secret.clone())318	}319	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {320		let config_field = &self.config_unchecked_field;321		Ok(nix_go_json!(322			config_field.sharedSecrets[{ secret }].expectedOwners323		))324	}325326	pub fn save(&self) -> Result<()> {327		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;328		let data = nixlike::serialize(&self.data() as &FleetData)?;329		tempfile.write_all(330			format!(331				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",332				data333			)334			.as_bytes(),335		)?;336		let mut fleet_data_path = self.directory.clone();337		fleet_data_path.push("fleet.nix");338		tempfile.persist(fleet_data_path)?;339		Ok(())340	}341}342343#[derive(Parser, Clone)]344#[clap(group = ArgGroup::new("target_hosts"))]345pub struct FleetOpts {346	/// All hosts except those would be skipped347	#[clap(long, number_of_values = 1, group = "target_hosts")]348	only: Vec<String>,349350	/// Hosts to skip351	#[clap(long, number_of_values = 1, group = "target_hosts")]352	skip: Vec<String>,353354	/// Host, which should be threaten as current machine355	#[clap(long)]356	pub localhost: Option<String>,357358	/// Override detected system for host, to perform builds via359	/// binfmt-declared qemu instead of trying to crosscompile360	#[clap(long, default_value = "detect")]361	pub local_system: String,362}363364impl FleetOpts {365	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {366		if self.localhost.is_none() {367			self.localhost368				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());369		}370		let directory = current_dir()?;371372		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;373		let root_field = pool.get().await?;374375		let builtins_field = Field::field(root_field.clone(), "builtins").await?;376		if self.local_system == "detect" {377			self.local_system = nix_go_json!(builtins_field.currentSystem);378		}379		let local_system = self.local_system.clone();380381		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;382		let fleet_field = nix_go!(fleet_root.default);383384		let config_field = nix_go!(fleet_field.config);385		let config_unchecked_field = nix_go!(fleet_field.unchecked.config);386387		let import = nix_go!(builtins_field.import);388		let overlays = nix_go!(fleet_field.overlays);389		let nixpkgs = nix_go!(fleet_field.nixpkgs | import);390391		let default_pkgs = nix_go!(nixpkgs(Obj {392			overlays,393			system: { self.local_system.clone() },394		}));395396		let mut fleet_data_path = directory.clone();397		fleet_data_path.push("fleet.nix");398		let bytes = std::fs::read_to_string(fleet_data_path)?;399		let data = nixlike::parse_str(&bytes)?;400401		Ok(Config(Arc::new(FleetConfigInternals {402			opts: self,403			directory,404			data,405			local_system,406			nix_args,407			config_field,408			config_unchecked_field,409			default_pkgs,410		})))411	}412}