git.delta.rocks / jrsonnet / refs/commits / 97d9be65842c

difftreelog

feat libssh preparation

Yaroslav Bolyukin2023-11-17parent: #3f73827.patch.diff
in: trunk

7 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -651,6 +651,27 @@
 ]
 
 [[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
 name = "displaydoc"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,13 +752,16 @@
  "clap",
  "futures",
  "hostname",
+ "human-repr",
  "indicatif",
  "itertools",
  "nixlike",
  "once_cell",
+ "openssh",
  "owo-colors",
  "peg",
  "r2d2",
+ "regex",
  "serde",
  "serde_json",
  "shlex",
@@ -1019,6 +1043,12 @@
 ]
 
 [[package]]
+name = "human-repr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1"
+
+[[package]]
 name = "humantime"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1257,6 +1287,17 @@
 ]
 
 [[package]]
+name = "libredox"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
+dependencies = [
+ "bitflags 2.4.1",
+ "libc",
+ "redox_syscall 0.4.1",
+]
+
+[[package]]
 name = "linked-hash-map"
 version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1480,6 +1521,28 @@
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
+name = "openssh"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfe68c42d6ee6bd9de175b7a5d9bb86aa99d4e2fa7cf2f2a44e97f60b6d2759"
+dependencies = [
+ "dirs",
+ "libc",
+ "once_cell",
+ "shell-escape",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-pipe",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
 name = "overload"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1804,15 +1867,26 @@
 ]
 
 [[package]]
+name = "redox_users"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
+dependencies = [
+ "getrandom 0.2.10",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
 name = "regex"
-version = "1.9.5"
+version = "1.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata 0.3.8",
- "regex-syntax 0.7.5",
+ "regex-automata 0.4.3",
+ "regex-syntax 0.8.2",
 ]
 
 [[package]]
@@ -1826,13 +1900,13 @@
 
 [[package]]
 name = "regex-automata"
-version = "0.3.8"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.7.5",
+ "regex-syntax 0.8.2",
 ]
 
 [[package]]
@@ -1843,9 +1917,9 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.7.5"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
 
 [[package]]
 name = "rnix"
@@ -2099,6 +2173,12 @@
 ]
 
 [[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
+[[package]]
 name = "shlex"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2379,6 +2459,16 @@
 ]
 
 [[package]]
+name = "tokio-pipe"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784"
+dependencies = [
+ "libc",
+ "tokio",
+]
+
+[[package]]
 name = "tokio-util"
 version = "0.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -11,7 +11,7 @@
 serde_json = "1.0"
 time = { version = "0.3.30", features = ["serde"] }
 tempfile = "3.8"
-once_cell = "1.18"
+once_cell = "1.18.0"
 hostname = "0.3.1"
 age-core = "0.9.0"
 peg = "0.8.2"
@@ -41,3 +41,6 @@
 r2d2 = "0.8.10"
 abort-on-drop = "0.2.2"
 unindent = "0.2.3"
+regex = "1.10.2"
+openssh = "0.10.1"
+human-repr = "1.1.0"
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -336,15 +336,14 @@
 				if !config.is_local(&host) {
 					info!("uploading system closure");
 					{
-						let mut sign = MyCommand::new("sudo");
+						let mut sign = MyCommand::new("nix");
 						// Private key for host machine is registered in nix-sign.nix
-						sign.arg("nix")
-							.arg("store")
+						sign.arg("store")
 							.arg("sign")
 							.comparg("--key-file", "/etc/nix/private-key")
 							.arg("-r")
 							.arg(&built);
-						if let Err(e) = sign.run_nix().await {
+						if let Err(e) = sign.sudo().run_nix().await {
 							warn!("Failed to sign store paths: {e}");
 						};
 					}
modifiedcmds/fleet/src/command.rsdiffbeforeafterboth
--- a/cmds/fleet/src/command.rs
+++ b/cmds/fleet/src/command.rs
@@ -1,4 +1,5 @@
 use std::{
+	borrow::Cow,
 	collections::HashMap,
 	ffi::OsStr,
 	process::Stdio,
@@ -6,8 +7,12 @@
 	task::Poll,
 };
 
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use futures::StreamExt;
+use itertools::Either;
+use once_cell::sync::Lazy;
+use openssh::{OverSsh, Session};
+use regex::Regex;
 use serde::{de::Visitor, Deserialize};
 use tokio::{io::AsyncRead, process::Command, select};
 use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
@@ -37,6 +42,7 @@
 	command: String,
 	args: Vec<String>,
 	env: Vec<(String, String)>,
+	ssh_session: Option<Arc<Session>>,
 }
 impl MyCommand {
 	pub fn new(cmd: impl AsRef<OsStr>) -> Self {
@@ -45,6 +51,7 @@
 			command: ostoutf8(cmd),
 			args: vec![],
 			env: vec![],
+			ssh_session: None,
 		}
 	}
 	fn into_args(self) -> Vec<String> {
@@ -90,6 +97,18 @@
 		}
 		out
 	}
+	fn into_command_new(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {
+		Ok(if let Some(session) = self.ssh_session.clone() {
+			let cmd = self.into_command();
+			Either::Right(
+				cmd.over_ssh(session)
+					.map_err(|e| anyhow!("ssh error: {e}"))?,
+			)
+		} else {
+			let cmd = self.into_command();
+			Either::Left(cmd)
+		})
+	}
 	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
 		let arg = arg.as_ref();
 		self.args.push(ostoutf8(arg));
@@ -116,9 +135,15 @@
 		self
 	}
 	pub fn sudo(self) -> Self {
-		let mut out = Self::new("sudo");
-		out.args(self.into_args());
-		out
+		if std::env::var_os("NO_SUDO").is_some() {
+			let mut out = Self::new("su");
+			out.arg("-c").arg(self.into_string());
+			out
+		} else {
+			let mut out = Self::new("sudo");
+			out.args(self.into_args());
+			out
+		}
 	}
 	pub fn ssh(self, on: impl AsRef<OsStr>) -> Self {
 		let mut out = Self::new("ssh");
@@ -126,6 +151,10 @@
 		out.arg(self.into_string());
 		out
 	}
+	pub fn over_ssh(mut self, session: Arc<Session>) -> Self {
+		self.ssh_session = Some(session);
+		self
+	}
 
 	pub async fn run(self) -> Result<()> {
 		let str = self.clone().into_string();
@@ -218,6 +247,11 @@
 pub struct NixHandler {
 	spans: HashMap<u64, Span>,
 }
+fn process_message(m: &str) -> Cow<'_, str> {
+	static OSC_CLEANER: Lazy<Regex> =
+		Lazy::new(|| Regex::new(r"\x1B\]([^\x07\x1C]*[\x07\x1C])?|\r").unwrap());
+	OSC_CLEANER.replace_all(m, "")
+}
 impl Handler for NixHandler {
 	fn handle_line(&mut self, e: &str) {
 		if let Some(e) = e.strip_prefix("@nix ") {
@@ -303,7 +337,7 @@
 					{
 						let span = info_span!("job");
 						span.pb_start();
-						span.pb_set_message(text.trim());
+						span.pb_set_message(&process_message(text.trim()));
 						self.spans.insert(id, span);
 						info!(target: "nix", "{}", text);
 					}
@@ -383,7 +417,7 @@
 				NixLog::Result { fields, id, typ } if typ == 101 && !fields.is_empty() => {
 					if let Some(span) = self.spans.get(&id) {
 						if let LogField::String(s) = &fields[0] {
-							span.pb_set_message(s.trim());
+							span.pb_set_message(&process_message(s.trim()));
 						} else {
 							warn!("bad fields: {fields:?}");
 						}
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
before · cmds/fleet/src/host.rs
1use std::{2	env::current_dir,3	ffi::OsString,4	io::Write,5	ops::Deref,6	path::PathBuf,7	sync::{Arc, Mutex, MutexGuard},8};910use anyhow::{bail, Context, Result};11use clap::{ArgGroup, Parser};12use tempfile::NamedTempFile;1314use crate::{15	better_nix_eval::{Field, NixSessionPool},16	command::MyCommand,17	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},18};1920pub struct FleetConfigInternals {21	pub local_system: String,22	pub directory: PathBuf,23	pub opts: FleetOpts,24	pub data: Mutex<FleetData>,25	pub nix_args: Vec<OsString>,26	// fleetConfigurations.<name>27	pub fleet_field: Field,28	// fleet_config.configUnchecked29	pub config_field: Field,30}3132#[derive(Clone)]33pub struct Config(Arc<FleetConfigInternals>);3435impl Deref for Config {36	type Target = FleetConfigInternals;3738	fn deref(&self) -> &Self::Target {39		&self.040	}41}4243pub struct ConfigHost {44	pub name: String,45}4647impl Config {48	pub fn should_skip(&self, host: &str) -> bool {49		if !self.opts.skip.is_empty() {50			self.opts.skip.iter().any(|h| h as &str == host)51		} else if !self.opts.only.is_empty() {52			!self.opts.only.iter().any(|h| h as &str == host)53		} else {54			false55		}56	}57	pub fn is_local(&self, host: &str) -> bool {58		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)59	}6061	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {62		if sudo {63			command = command.sudo();64		}65		if !self.is_local(host) {66			command = command.ssh(host);67		}68		command.run().await69	}70	pub async fn run_string_on(71		&self,72		host: &str,73		mut command: MyCommand,74		sudo: bool,75	) -> Result<String> {76		if sudo {77			command = command.sudo();78		}79		if !self.is_local(host) {80			command = command.ssh(host);81		}82		command.run_string().await83	}8485	pub fn configuration_attr_name(&self, name: &str) -> OsString {86		let mut str = self.directory.as_os_str().to_owned();87		str.push("#");88		str.push(&format!(89			"fleetConfigurations.default.{}.{}",90			self.local_system, name91		));92		str93	}9495	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {96		let names = self.fleet_field97			.get_field_deep(["configuredHosts"])98			.await?99			.list_fields()100			.await?;101		 let mut out = vec![];102		 for name in names {103			out.push(ConfigHost {104				name,105			})106		 }107		 Ok(out)108	}109	pub async fn system_config(&self, host: &str) -> Result<Field> {110		self.fleet_field.get_field_deep(["configuredSystems", host, "config"]).await111	}112113	pub(super) fn data(&self) -> MutexGuard<FleetData> {114		self.data.lock().unwrap()115	}116	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {117		self.data.lock().unwrap()118	}119	/// Shared secrets configured in fleet.nix or in flake120	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {121		self.config_field122			.get_field("sharedSecrets")123			.await?124			.list_fields()125			.await126	}127	/// Shared secrets configured in fleet.nix128	pub fn list_shared(&self) -> Vec<String> {129		let data = self.data();130		data.shared_secrets.keys().cloned().collect()131	}132	pub fn has_shared(&self, name: &str) -> bool {133		let data = self.data();134		data.shared_secrets.contains_key(name)135	}136	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {137		let mut data = self.data_mut();138		data.shared_secrets.insert(name.to_owned(), shared);139	}140	pub fn remove_shared(&self, secret: &str) {141		let mut data = self.data_mut();142		data.shared_secrets.remove(secret);143	}144145	pub fn has_secret(&self, host: &str, secret: &str) -> bool {146		let data = self.data();147		let Some(host_secrets) = data.host_secrets.get(host) else {148			return false;149		};150		host_secrets.contains_key(secret)151	}152	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {153		let mut data = self.data_mut();154		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();155		host_secrets.insert(secret, value);156	}157158	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {159		let data = z85::encode(&data);160		let mut cmd = MyCommand::new("fleet-install-secrets");161		cmd.arg("decrypt").eqarg("--secret", data);162		cmd = cmd.sudo().ssh(host);163		let encoded = cmd164			.run_string()165			.await166			.context("failed to call remote host for decrypt")?167			.trim()168			.to_owned();169		z85::decode(encoded).context("bad encoded data? outdated host?")170	}171	pub async fn reencrypt_on_host(172		&self,173		host: &str,174		data: Vec<u8>,175		targets: Vec<String>,176	) -> Result<Vec<u8>> {177		let data = z85::encode(&data);178		let mut recmd = MyCommand::new("fleet-install-secrets");179		recmd.arg("reencrypt").eqarg("--secret", data);180		for target in targets {181			recmd.eqarg("--targets", target);182		}183		recmd = recmd.sudo().ssh(host);184		let encoded = recmd185			.run_string()186			.await187			.context("failed to call remote host for decrypt")?188			.trim()189			.to_owned();190		z85::decode(encoded).context("bad encoded data? outdated host?")191	}192193	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {194		let data = self.data();195		let Some(host_secrets) = data.host_secrets.get(host) else {196			bail!("no secrets for machine {host}");197		};198		let Some(secret) = host_secrets.get(secret) else {199			bail!("machine {host} has no secret {secret}");200		};201		Ok(secret.clone())202	}203	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {204		let data = self.data();205		let Some(secret) = data.shared_secrets.get(secret) else {206			bail!("no shared secret {secret}");207		};208		Ok(secret.clone())209	}210	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {211		self.config_field212			.get_field_deep(["sharedSecrets", secret, "expectedOwners"])213			.await?214			.as_json()215			.await216	}217218	pub fn save(&self) -> Result<()> {219		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;220		let data = nixlike::serialize(&self.data() as &FleetData)?;221		tempfile.write_all(222			format!(223				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",224				data225			)226			.as_bytes(),227		)?;228		let mut fleet_data_path = self.directory.clone();229		fleet_data_path.push("fleet.nix");230		tempfile.persist(fleet_data_path)?;231		Ok(())232	}233}234235#[derive(Parser, Clone)]236#[clap(group = ArgGroup::new("target_hosts"))]237pub struct FleetOpts {238	/// All hosts except those would be skipped239	#[clap(long, number_of_values = 1, group = "target_hosts")]240	only: Vec<String>,241242	/// Hosts to skip243	#[clap(long, number_of_values = 1, group = "target_hosts")]244	skip: Vec<String>,245246	/// Host, which should be threaten as current machine247	#[clap(long)]248	pub localhost: Option<String>,249250	// TODO: unhardcode x86_64-linux251	/// Override detected system for host, to perform builds via252	/// binfmt-declared qemu instead of trying to crosscompile253	#[clap(long, default_value = "detect")]254	pub local_system: String,255}256257impl FleetOpts {258	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {259		if self.localhost.is_none() {260			self.localhost261				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());262		}263		let directory = current_dir()?;264265		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;266		let root_field = pool.get().await?;267268		if self.local_system == "detect" {269			let builtins_field = Field::field(root_field.clone(), "builtins").await?;270			let system = builtins_field.get_field("currentSystem").await?;271			self.local_system = system.as_json().await?;272		}273		let local_system = self.local_system.clone();274275		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;276277		let fleet_field = fleet_root278			.get_field_deep(["default", &local_system])279			.await?;280		let config_field = fleet_field.get_field("configUnchecked").await?;281282		let mut fleet_data_path = directory.clone();283		fleet_data_path.push("fleet.nix");284		let bytes = std::fs::read_to_string(fleet_data_path)?;285		let data = nixlike::parse_str(&bytes)?;286287		Ok(Config(Arc::new(FleetConfigInternals {288			opts: self,289			directory,290			data,291			local_system,292			nix_args,293			fleet_field,294			config_field,295		})))296	}297}
after · cmds/fleet/src/host.rs
1use std::{2	env::current_dir,3	ffi::OsString,4	io::Write,5	ops::Deref,6	path::PathBuf,7	sync::{Arc, Mutex, MutexGuard},8};910use anyhow::{anyhow, bail, Context, Result};11use clap::{ArgGroup, Parser};12use openssh::SessionBuilder;13use tempfile::NamedTempFile;1415use crate::{16	better_nix_eval::{Field, NixSessionPool},17	command::MyCommand,18	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19};2021pub struct FleetConfigInternals {22	pub local_system: String,23	pub directory: PathBuf,24	pub opts: FleetOpts,25	pub data: Mutex<FleetData>,26	pub nix_args: Vec<OsString>,27	// fleetConfigurations.<name>28	pub fleet_field: Field,29	// fleet_config.configUnchecked30	pub config_field: Field,31}3233#[derive(Clone)]34pub struct Config(Arc<FleetConfigInternals>);3536impl Deref for Config {37	type Target = FleetConfigInternals;3839	fn deref(&self) -> &Self::Target {40		&self.041	}42}4344pub struct ConfigHost {45	pub name: String,46}47impl ConfigHost {48	async fn open_session(&self) -> Result<openssh::Session> {49		let mut session = SessionBuilder::default();5051		session52			.connect(&self.name)53			.await54			.map_err(|e| anyhow!("ssh error: {e}"))55	}56}5758impl Config {59	pub fn should_skip(&self, host: &str) -> bool {60		if !self.opts.skip.is_empty() {61			self.opts.skip.iter().any(|h| h as &str == host)62		} else if !self.opts.only.is_empty() {63			!self.opts.only.iter().any(|h| h as &str == host)64		} else {65			false66		}67	}68	pub fn is_local(&self, host: &str) -> bool {69		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)70	}7172	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {73		if sudo {74			command = command.sudo();75		}76		if !self.is_local(host) {77			command = command.ssh(host);78		}79		command.run().await80	}81	pub async fn run_string_on(82		&self,83		host: &str,84		mut command: MyCommand,85		sudo: bool,86	) -> Result<String> {87		if sudo {88			command = command.sudo();89		}90		if !self.is_local(host) {91			command = command.ssh(host);92		}93		command.run_string().await94	}9596	pub fn configuration_attr_name(&self, name: &str) -> OsString {97		let mut str = self.directory.as_os_str().to_owned();98		str.push("#");99		str.push(&format!(100			"fleetConfigurations.default.{}.{}",101			self.local_system, name102		));103		str104	}105106	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {107		let names = self108			.fleet_field109			.get_field_deep(["configuredHosts"])110			.await?111			.list_fields()112			.await?;113		let mut out = vec![];114		for name in names {115			out.push(ConfigHost { name })116		}117		Ok(out)118	}119	pub async fn system_config(&self, host: &str) -> Result<Field> {120		self.fleet_field121			.get_field_deep(["configuredSystems", host, "config"])122			.await123	}124125	pub(super) fn data(&self) -> MutexGuard<FleetData> {126		self.data.lock().unwrap()127	}128	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {129		self.data.lock().unwrap()130	}131	/// Shared secrets configured in fleet.nix or in flake132	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {133		self.config_field134			.get_field("sharedSecrets")135			.await?136			.list_fields()137			.await138	}139	/// Shared secrets configured in fleet.nix140	pub fn list_shared(&self) -> Vec<String> {141		let data = self.data();142		data.shared_secrets.keys().cloned().collect()143	}144	pub fn has_shared(&self, name: &str) -> bool {145		let data = self.data();146		data.shared_secrets.contains_key(name)147	}148	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {149		let mut data = self.data_mut();150		data.shared_secrets.insert(name.to_owned(), shared);151	}152	pub fn remove_shared(&self, secret: &str) {153		let mut data = self.data_mut();154		data.shared_secrets.remove(secret);155	}156157	pub fn has_secret(&self, host: &str, secret: &str) -> bool {158		let data = self.data();159		let Some(host_secrets) = data.host_secrets.get(host) else {160			return false;161		};162		host_secrets.contains_key(secret)163	}164	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {165		let mut data = self.data_mut();166		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();167		host_secrets.insert(secret, value);168	}169170	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {171		let data = z85::encode(&data);172		let mut cmd = MyCommand::new("fleet-install-secrets");173		cmd.arg("decrypt").eqarg("--secret", data);174		cmd = cmd.sudo().ssh(host);175		let encoded = cmd176			.run_string()177			.await178			.context("failed to call remote host for decrypt")?179			.trim()180			.to_owned();181		z85::decode(encoded).context("bad encoded data? outdated host?")182	}183	pub async fn reencrypt_on_host(184		&self,185		host: &str,186		data: Vec<u8>,187		targets: Vec<String>,188	) -> Result<Vec<u8>> {189		let data = z85::encode(&data);190		let mut recmd = MyCommand::new("fleet-install-secrets");191		recmd.arg("reencrypt").eqarg("--secret", data);192		for target in targets {193			recmd.eqarg("--targets", target);194		}195		recmd = recmd.sudo().ssh(host);196		let encoded = recmd197			.run_string()198			.await199			.context("failed to call remote host for decrypt")?200			.trim()201			.to_owned();202		z85::decode(encoded).context("bad encoded data? outdated host?")203	}204205	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {206		let data = self.data();207		let Some(host_secrets) = data.host_secrets.get(host) else {208			bail!("no secrets for machine {host}");209		};210		let Some(secret) = host_secrets.get(secret) else {211			bail!("machine {host} has no secret {secret}");212		};213		Ok(secret.clone())214	}215	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {216		let data = self.data();217		let Some(secret) = data.shared_secrets.get(secret) else {218			bail!("no shared secret {secret}");219		};220		Ok(secret.clone())221	}222	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {223		self.config_field224			.get_field_deep(["sharedSecrets", secret, "expectedOwners"])225			.await?226			.as_json()227			.await228	}229230	pub fn save(&self) -> Result<()> {231		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;232		let data = nixlike::serialize(&self.data() as &FleetData)?;233		tempfile.write_all(234			format!(235				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",236				data237			)238			.as_bytes(),239		)?;240		let mut fleet_data_path = self.directory.clone();241		fleet_data_path.push("fleet.nix");242		tempfile.persist(fleet_data_path)?;243		Ok(())244	}245}246247#[derive(Parser, Clone)]248#[clap(group = ArgGroup::new("target_hosts"))]249pub struct FleetOpts {250	/// All hosts except those would be skipped251	#[clap(long, number_of_values = 1, group = "target_hosts")]252	only: Vec<String>,253254	/// Hosts to skip255	#[clap(long, number_of_values = 1, group = "target_hosts")]256	skip: Vec<String>,257258	/// Host, which should be threaten as current machine259	#[clap(long)]260	pub localhost: Option<String>,261262	// TODO: unhardcode x86_64-linux263	/// Override detected system for host, to perform builds via264	/// binfmt-declared qemu instead of trying to crosscompile265	#[clap(long, default_value = "detect")]266	pub local_system: String,267}268269impl FleetOpts {270	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {271		if self.localhost.is_none() {272			self.localhost273				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());274		}275		let directory = current_dir()?;276277		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;278		let root_field = pool.get().await?;279280		if self.local_system == "detect" {281			let builtins_field = Field::field(root_field.clone(), "builtins").await?;282			let system = builtins_field.get_field("currentSystem").await?;283			self.local_system = system.as_json().await?;284		}285		let local_system = self.local_system.clone();286287		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;288289		let fleet_field = fleet_root290			.get_field_deep(["default", &local_system])291			.await?;292		let config_field = fleet_field.get_field("configUnchecked").await?;293294		let mut fleet_data_path = directory.clone();295		fleet_data_path.push("fleet.nix");296		let bytes = std::fs::read_to_string(fleet_data_path)?;297		let data = nixlike::parse_str(&bytes)?;298299		Ok(Config(Arc::new(FleetConfigInternals {300			opts: self,301			directory,302			data,303			local_system,304			nix_args,305			fleet_field,306			config_field,307		})))308	}309}
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -5,8 +5,8 @@
 pub(crate) mod host;
 pub(crate) mod keys;
 
+pub(crate) mod better_nix_eval;
 pub(crate) mod extra_args;
-pub(crate) mod better_nix_eval;
 
 mod fleetdata;
 
@@ -21,6 +21,7 @@
 use futures::stream::FuturesUnordered;
 use futures::TryStreamExt;
 use host::{Config, FleetOpts};
+use human_repr::HumanCount;
 use indicatif::{ProgressState, ProgressStyle};
 use tracing::{info, metadata::LevelFilter};
 use tracing::{info_span, Instrument};
@@ -121,9 +122,16 @@
 fn setup_logging() {
 	let indicatif_layer = IndicatifLayer::new().with_progress_style(
 		ProgressStyle::with_template(
-			"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{pos:>7}/{len:7}{elapsed}{color_end}",
+			"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",
 		)
 		.unwrap()
+		.with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
+			let Some(len) = state.len() else {
+				return;
+			};
+			let pos = state.pos();
+			let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());
+		})
 		.with_key(
 			"color_start",
 			|state: &ProgressState, writer: &mut dyn std::fmt::Write| {
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,44 +1,56 @@
-{ flake-utils }: {
-  fleetConfiguration = { data, nixpkgs, hosts, ... }@allConfig:
-    let
-      hostNames = nixpkgs.lib.attrNames hosts;
-      config = builtins.removeAttrs allConfig [ "nixpkgs" "data" ];
-      fleetLib = import ./fleetLib.nix {
-        inherit nixpkgs hostNames;
-      };
-    in
-    nixpkgs.lib.genAttrs flake-utils.lib.defaultSystems (system:
-      let
+{flake-utils}: {
+  fleetConfiguration = {
+    data,
+    nixpkgs,
+    hosts,
+    ...
+  } @ allConfig: let
+    hostNames = nixpkgs.lib.attrNames hosts;
+    config = builtins.removeAttrs allConfig ["nixpkgs" "data"];
+    fleetLib = import ./fleetLib.nix {
+      inherit nixpkgs hostNames;
+    };
+  in
+    # Top-level arg is the builder system (not the target system!)
+    nixpkgs.lib.genAttrs flake-utils.lib.defaultSystems (system: let
+      withData = data: rec {
         root = nixpkgs.lib.evalModules {
-          modules = (import ../modules/fleet/_modules.nix) ++ [ config data ];
+          modules = (import ../modules/fleet/_modules.nix) ++ [config data];
           specialArgs = {
             inherit nixpkgs fleetLib;
           };
         };
         failedAssertions = map (x: x.message) (nixpkgs.lib.filter (x: !x.assertion) root.config.assertions);
         rootAssertWarn =
-          if failedAssertions != [ ]
+          if failedAssertions != []
           then throw "Failed assertions:\n${nixpkgs.lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
           else nixpkgs.lib.showWarnings root.config.warnings root;
         configuredHosts = rootAssertWarn.config.hosts;
         configuredSecrets = rootAssertWarn.config.secrets;
-        configuredSystems = configuredSystemsWithExtraModules [ ];
-        configuredSystemsWithExtraModules = extraModules: nixpkgs.lib.listToAttrs (
-          map
+        configuredSystems = configuredSystemsWithExtraModules [];
+        configuredSystemsWithExtraModules = extraModules:
+          nixpkgs.lib.listToAttrs (
+            map
             (
               name: {
                 inherit name;
                 value = nixpkgs.lib.nixosSystem {
                   system = configuredHosts.${name}.system;
-                  modules = configuredHosts.${name}.modules ++ extraModules ++ [
-                    ({ ... }: {
-                      nixpkgs.system = system;
-                      nixpkgs.localSystem.system = system;
-                      nixpkgs.crossSystem = if system == configuredHosts.${name}.system then null else {
-                        system = configuredHosts.${name}.system;
-                      };
-                    })
-                  ];
+                  modules =
+                    configuredHosts.${name}.modules
+                    ++ extraModules
+                    ++ [
+                      ({...}: {
+                        nixpkgs.system = system;
+                        nixpkgs.localSystem.system = system;
+                        nixpkgs.crossSystem =
+                          if system == configuredHosts.${name}.system
+                          then null
+                          else {
+                            system = configuredHosts.${name}.system;
+                          };
+                      })
+                    ];
                   specialArgs = {
                     inherit fleetLib;
                     fleet = fleetLib.hostsToAttrs (host: configuredSystems.${host}.config);
@@ -47,11 +59,7 @@
               }
             )
             (builtins.attrNames rootAssertWarn.config.hosts)
-        );
-      in
-      rec {
-        inherit configuredHosts configuredSecrets configuredSystems;
-        configUnchecked = root.config;
+          );
         buildSystems = {
           toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [
             ({...}: {
@@ -66,12 +74,22 @@
           ]);
           installationCd = builtins.mapAttrs (_name: value: value.config.system.build.isoImage) (configuredSystemsWithExtraModules [
             (nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
-            ({ lib, ... }: {
+            ({lib, ...}: {
               buildTarget = "installation-cd";
               # Needed for https://github.com/NixOS/nixpkgs/issues/58959
-              boot.supportedFilesystems = lib.mkForce [ "btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs" ];
+              boot.supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"];
             })
           ]);
         };
-      });
+        configUnchecked = root.config;
+      };
+      defaultData = withData data;
+    in rec {
+      inherit (defaultData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
+      injectData = data: let
+        injectedData = withData data;
+      in {
+        inherit (injectedData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
+      };
+    });
 }