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

difftreelog

refactor split build-systems and deploy commands

Yaroslav Bolyukin2024-01-05parent: #718d88b.patch.diff
in: trunk

10 files changed

modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,3 +5,5 @@
 [workspace.dependencies]
 nixlike = { path = "./crates/nixlike" }
 better-command = { path = "./crates/better-command" }
+uuid = { version = "1.3.3", features = ["v4"] }
+tokio = { version = "1.33.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -8,6 +8,7 @@
 [dependencies]
 nixlike.workspace = true
 better-command.workspace = true
+tokio.workspace = true
 anyhow = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
@@ -27,7 +28,6 @@
 	"wrap_help",
 	"unicode",
 ] }
-tokio = { version = "1.33.0", features = ["full"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
 tokio-util = { version = "0.7.10", features = ["codec"] }
modifiedcmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth
--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -428,6 +428,7 @@
 		self.used_fields.extend(e.used_fields);
 	}
 
+	#[allow(dead_code)]
 	pub fn session(&self) -> NixSession {
 		let mut session = None;
 		for ele in &self.used_fields {
@@ -444,6 +445,7 @@
 		}
 		session.expect("expr without fields used")
 	}
+	#[allow(dead_code)]
 	pub fn index_attr(&mut self, s: &str) {
 		let escaped = nixlike::serialize(s).expect("string");
 		self.out.push('.');
@@ -559,7 +561,9 @@
 pub enum Index {
 	Var(String),
 	String(String),
+	#[allow(dead_code)]
 	Apply(String),
+	#[allow(dead_code)]
 	Expr(NixExprBuilder),
 	ExprApply(NixExprBuilder),
 	Pipe(NixExprBuilder),
@@ -576,6 +580,7 @@
 	pub fn attr(v: impl AsRef<str>) -> Self {
 		Self::String(v.as_ref().to_owned())
 	}
+	#[allow(dead_code)]
 	pub fn apply(v: impl Serialize) -> Self {
 		let serialized = nixlike::serialize(v).expect("invalid value for apply");
 		Self::Apply(serialized.trim_end().to_owned())
@@ -749,6 +754,7 @@
 			.await
 			.with_context(|| context("as_json", self.0.full_path.as_deref(), &query))
 	}
+	#[allow(dead_code)]
 	pub async fn has_field(&self, name: &str) -> Result<bool> {
 		let id = self.0.value.expect("can't list root fields");
 		let key = nixlike::escape_string(name);
@@ -786,6 +792,7 @@
 			.await
 			.with_context(|| context("type_of", self.0.full_path.as_deref(), &query))
 	}
+	#[allow(dead_code)]
 	pub async fn import(&self) -> Result<Self> {
 		let import = Self::new(self.0.session.clone(), "import").await?;
 		Ok(nix_go!(self | import))
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/build_systems.rs
1use std::os::unix::fs::symlink;2use std::path::PathBuf;3use std::{env::current_dir, time::Duration};45use crate::command::MyCommand;6use crate::host::{Config, ConfigHost};7use crate::nix_go;8use anyhow::{anyhow, Result};9use clap::Parser;10use itertools::Itertools as _;11use tokio::{task::LocalSet, time::sleep};12use tracing::{error, field, info, info_span, warn, Instrument};1314#[derive(Parser, Clone)]15pub struct BuildSystems {16	/// Disable automatic rollback17	#[clap(long)]18	disable_rollback: bool,19	#[clap(subcommand)]20	subcommand: Subcommand,21}2223enum UploadAction {24	Test,25	Boot,26	Switch,27}28impl UploadAction {29	fn name(&self) -> &'static str {30		match self {31			UploadAction::Test => "test",32			UploadAction::Boot => "boot",33			UploadAction::Switch => "switch",34		}35	}3637	pub(crate) fn should_switch_profile(&self) -> bool {38		matches!(self, Self::Switch | Self::Boot)39	}40	pub(crate) fn should_activate(&self) -> bool {41		matches!(self, Self::Switch | Self::Test)42	}43	pub(crate) fn should_schedule_rollback_run(&self) -> bool {44		matches!(self, Self::Switch | Self::Test)45	}46}4748enum PackageAction {49	SdImage,50	InstallationCd,51}52impl PackageAction {53	fn build_attr(&self) -> String {54		match self {55			PackageAction::SdImage => "sdImage".to_owned(),56			PackageAction::InstallationCd => "isoImage".to_owned(),57		}58	}59}6061enum Action {62	Upload { action: Option<UploadAction> },63	Package(PackageAction),64}65impl Action {66	fn build_attr(&self) -> String {67		match self {68			Action::Upload { .. } => "toplevel".to_owned(),69			Action::Package(p) => p.build_attr(),70		}71	}72}7374impl From<Subcommand> for Action {75	fn from(s: Subcommand) -> Self {76		match s {77			Subcommand::Upload => Self::Upload { action: None },78			Subcommand::Test => Self::Upload {79				action: Some(UploadAction::Test),80			},81			Subcommand::Boot => Self::Upload {82				action: Some(UploadAction::Boot),83			},84			Subcommand::Switch => Self::Upload {85				action: Some(UploadAction::Switch),86			},87			Subcommand::SdImage => Self::Package(PackageAction::SdImage),88			Subcommand::InstallationCd => Self::Package(PackageAction::InstallationCd),89		}90	}91}9293#[derive(Parser, Clone)]94enum Subcommand {95	/// Upload, but do not switch96	Upload,97	/// Upload + switch to built system until reboot98	Test,99	/// Upload + switch to built system after reboot100	Boot,101	/// Upload + test + boot102	Switch,103104	/// Build SD .img image105	SdImage,106	/// Build an installation cd ISO image107	InstallationCd,108}109110struct Generation {111	id: u32,112	current: bool,113	datetime: String,114}115async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {116	let mut cmd = host.cmd("nix-env").await?;117	cmd.comparg("--profile", "/nix/var/nix/profiles/system")118		.arg("--list-generations");119	// Sudo is required due to --list-generations acquiring lock on the profile.120	let data = cmd.sudo().run_string().await?;121	let generations = data122		.split('\n')123		.map(|e| e.trim())124		.filter(|&l| !l.is_empty())125		.filter_map(|g| {126			let gen: Option<Generation> = try {127				let mut parts = g.split_whitespace();128				let id = parts.next()?;129				let id: u32 = id.parse().ok()?;130				let date = parts.next()?;131				let time = parts.next()?;132				let current = if let Some(current) = parts.next() {133					if current == "(current)" {134						Some(true)135					} else {136						None137					}138				} else {139					Some(false)140				};141				let current = current?;142				if parts.next().is_some() {143					warn!("unexpected text after generation: {g}");144				}145				Generation {146					id,147					current,148					datetime: format!("{date} {time}"),149				}150			};151			if gen.is_none() {152				warn!("bad generation: {g}")153			}154			gen155		})156		.collect::<Vec<_>>();157	let current = generations158		.into_iter()159		.filter(|g| g.current)160		.at_most_one()161		.map_err(|_e| anyhow!("bad list-generations output"))?162		.ok_or_else(|| anyhow!("failed to find generation"))?;163	Ok(current)164}165166async fn execute_upload(167	build: &BuildSystems,168	action: UploadAction,169	host: &ConfigHost,170	built: PathBuf,171) -> Result<()> {172	let mut failed = false;173	// TODO: Lockfile, to prevent concurrent system switch?174	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback175	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to176	// unit name conflict in systemd-run177	// This code is tied to rollback.nix178	if !build.disable_rollback {179		let _span = info_span!("preparing").entered();180		info!("preparing for rollback");181		let generation = get_current_generation(host).await?;182		info!(183			"rollback target would be {} {}",184			generation.id, generation.datetime185		);186		{187			let mut cmd = host.cmd("sh").await?;188			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));189			if let Err(e) = cmd.sudo().run().await {190				error!("failed to set rollback marker: {e}");191				failed = true;192			}193		}194		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.195		// Kicking it on manually will work best.196		//197		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will198		// only allow one instance of it.199200		// TODO: We should also watch how this process is going.201		// After running this command, we have less than 3 minutes to deploy everything,202		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.203		// Anyway, reboot will still help in this case.204		if action.should_schedule_rollback_run() {205			let mut cmd = host.cmd("systemd-run").await?;206			cmd.comparg("--on-active", "3min")207				.comparg("--unit", "rollback-watchdog-run")208				.arg("systemctl")209				.arg("start")210				.arg("rollback-watchdog.service");211			if let Err(e) = cmd.sudo().run().await {212				error!("failed to schedule rollback run: {e}");213				failed = true;214			}215		}216	}217218	if action.should_switch_profile() && !failed {219		info!("switching generation");220		let mut cmd = host.cmd("nix-env").await?;221		cmd.comparg("--profile", "/nix/var/nix/profiles/system")222			.comparg("--set", &built);223		if let Err(e) = cmd.sudo().run().await {224			error!("failed to switch generation: {e}");225			failed = true;226		}227	}228229	// FIXME: Connection might be disconnected after activation run230231	if action.should_activate() && !failed {232		let _span = info_span!("activating").entered();233		info!("executing activation script");234		let mut switch_script = built.clone();235		switch_script.push("bin");236		switch_script.push("switch-to-configuration");237		let mut cmd = host.cmd(switch_script).in_current_span().await?;238		cmd.arg(action.name());239		if let Err(e) = cmd.sudo().run().in_current_span().await {240			error!("failed to activate: {e}");241			failed = true;242		}243	}244	if !build.disable_rollback {245		if failed {246			info!("executing rollback");247			if let Err(e) = host248				.systemctl_start("rollback-watchdog.service")249				.instrument(info_span!("rollback"))250				.await251			{252				error!("failed to trigger rollback: {e}")253			}254		} else {255			info!("trying to mark upgrade as successful");256			if let Err(e) = host257				.rm_file("/etc/fleet_rollback_marker", true)258				.in_current_span()259				.await260			{261				error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")262			}263		}264		info!("disarming watchdog, just in case");265		if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {266			// It is ok, if there was no reboot - then timer might not be running.267		}268		if action.should_schedule_rollback_run() {269			if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {270				error!("failed to disarm rollback run: {e}");271			}272		}273	} else if let Err(_e) = host274		.rm_file("/etc/fleet_rollback_marker", true)275		.in_current_span()276		.await277	{278		// Marker might not exist, yet better try to remove it.279	}280	Ok(())281}282283impl BuildSystems {284	async fn build_task(self, config: Config, host: String) -> Result<()> {285		info!("building");286		let host = config.host(&host).await?;287		let action = Action::from(self.subcommand.clone());288		let fleet_config = &config.config_field;289		let drv = nix_go!(290			fleet_config.hosts[{ &host.name }].nixosSystem.config.system.build[{ action.build_attr() }]291		);292		let outputs = drv.build().await.map_err(|e| {293			if action.build_attr() == "sdImage" {294				info!("sd-image build failed");295				info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");296			}297			e298		})?;299		let out_output = outputs300			.get("out")301			.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;302303		match action {304			Action::Upload { action } => {305				if !config.is_local(&host.name) {306					info!("uploading system closure");307					{308						// TODO: Move to remote_derivation method.309						// Alternatively, nix store make-content-addressed can be used,310						// at least for the first deployment, to provide trusted store key.311						//312						// It is much slower, yet doesn't require root on the deployer machine.313						let mut sign = MyCommand::new("nix");314						// Private key for host machine is registered in nix-sign.nix315						sign.arg("store")316							.arg("sign")317							.comparg("--key-file", "/etc/nix/private-key")318							.arg("-r")319							.arg(out_output);320						if let Err(e) = sign.sudo().run_nix().await {321							warn!("Failed to sign store paths: {e}");322						};323					}324					let mut tries = 0;325					loop {326						match host.remote_derivation(out_output).await {327							Ok(remote) => {328								assert!(&remote == out_output, "CA derivations aren't implemented");329								break;330							}331							Err(e) if tries < 3 => {332								tries += 1;333								warn!("Copy failure ({}/3): {}", tries, e);334								sleep(Duration::from_millis(5000)).await;335							}336							Err(e) => return Err(e),337						}338					}339				}340				if let Some(action) = action {341					execute_upload(&self, action, &host, out_output.clone()).await?342				}343			}344			Action::Package(PackageAction::SdImage) => {345				let mut out = current_dir()?;346				out.push(format!("sd-image-{}", host.name));347348				info!("linking sd image to {:?}", out);349				symlink(out_output, out)?;350			}351			Action::Package(PackageAction::InstallationCd) => {352				let mut out = current_dir()?;353				out.push(format!("installation-cd-{}", host.name));354355				info!("linking iso image to {:?}", out);356				symlink(out_output, out)?;357			}358		};359		Ok(())360	}361362	pub async fn run(self, config: &Config) -> Result<()> {363		let hosts = config.list_hosts().await?;364		let set = LocalSet::new();365		let this = &self;366		for host in hosts.into_iter() {367			if config.should_skip(&host.name) {368				continue;369			}370			let config = config.clone();371			let this = this.clone();372			let span = info_span!("deployment", host = field::display(&host.name));373			let hostname = host.name;374			// FIXME: Since the introduction of better-nix-eval,375			// due to single repl used for builds, hosts are waiting for each other to build,376			// instead of building concurrently.377			//378			// Open multiple repls?379			//380			// Create build batcher, which will behave similar to golangs381			// WaitGroup, and start executing once all the build tasks are scheduled?382			// This also allows to cleanup build output, as there will be no longer383			// "waiting for remote machine" messages in the cases when one package is needed for384			// multiple hosts.385			set.spawn_local(386				(async move {387					match this.build_task(config, hostname).await {388						Ok(_) => {}389						Err(e) => {390							error!("failed to deploy host: {}", e)391						}392					}393				})394				.instrument(span),395			);396		}397		set.await;398		Ok(())399	}400}
after · cmds/fleet/src/cmds/build_systems.rs
1use std::os::unix::fs::symlink;2use std::path::PathBuf;3use std::{env::current_dir, time::Duration};45use crate::command::MyCommand;6use crate::host::{Config, ConfigHost};7use crate::nix_go;8use anyhow::{anyhow, Result};9use clap::{Parser, ValueEnum};10use itertools::Itertools as _;11use tokio::{task::LocalSet, time::sleep};12use tracing::{error, field, info, info_span, warn, Instrument};1314#[derive(Parser)]15pub struct Deploy {16	/// Disable automatic rollback17	#[clap(long)]18	disable_rollback: bool,19	action: DeployAction,20}2122#[derive(ValueEnum, Clone, Copy)]23enum DeployAction {24	/// Upload derivation, but do not execute the update.25	Upload,26	/// Upload and execute the activation script, old version will be used after reboot.27	Test,28	/// Upload and set as current system profile, but do not execute activation script.29	Boot,30	/// Upload, set current profile, and execute activation script.31	Switch,32}3334impl DeployAction {35	pub(crate) fn name(&self) -> Option<&'static str> {36		match self {37			DeployAction::Upload => None,38			DeployAction::Test => Some("test"),39			DeployAction::Boot => Some("boot"),40			DeployAction::Switch => Some("switch"),41		}42	}43	pub(crate) fn should_switch_profile(&self) -> bool {44		matches!(self, Self::Switch | Self::Boot)45	}46	pub(crate) fn should_activate(&self) -> bool {47		matches!(self, Self::Switch | Self::Test)48	}49	pub(crate) fn should_schedule_rollback_run(&self) -> bool {50		matches!(self, Self::Switch | Self::Test)51	}52}5354#[derive(Parser, Clone)]55pub struct BuildSystems {56	/// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes57	/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.58	#[clap(long, default_value = "toplevel")]59	build_attr: String,60}6162struct Generation {63	id: u32,64	current: bool,65	datetime: String,66}67async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {68	let mut cmd = host.cmd("nix-env").await?;69	cmd.comparg("--profile", "/nix/var/nix/profiles/system")70		.arg("--list-generations");71	// Sudo is required due to --list-generations acquiring lock on the profile.72	let data = cmd.sudo().run_string().await?;73	let generations = data74		.split('\n')75		.map(|e| e.trim())76		.filter(|&l| !l.is_empty())77		.filter_map(|g| {78			let gen: Option<Generation> = try {79				let mut parts = g.split_whitespace();80				let id = parts.next()?;81				let id: u32 = id.parse().ok()?;82				let date = parts.next()?;83				let time = parts.next()?;84				let current = if let Some(current) = parts.next() {85					if current == "(current)" {86						Some(true)87					} else {88						None89					}90				} else {91					Some(false)92				};93				let current = current?;94				if parts.next().is_some() {95					warn!("unexpected text after generation: {g}");96				}97				Generation {98					id,99					current,100					datetime: format!("{date} {time}"),101				}102			};103			if gen.is_none() {104				warn!("bad generation: {g}")105			}106			gen107		})108		.collect::<Vec<_>>();109	let current = generations110		.into_iter()111		.filter(|g| g.current)112		.at_most_one()113		.map_err(|_e| anyhow!("bad list-generations output"))?114		.ok_or_else(|| anyhow!("failed to find generation"))?;115	Ok(current)116}117118async fn deploy_task(119	action: DeployAction,120	host: &ConfigHost,121	built: PathBuf,122	disable_rollback: bool,123) -> Result<()> {124	let mut failed = false;125	// TODO: Lockfile, to prevent concurrent system switch?126	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback127	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to128	// unit name conflict in systemd-run129	// This code is tied to rollback.nix130	if !disable_rollback {131		let _span = info_span!("preparing").entered();132		info!("preparing for rollback");133		let generation = get_current_generation(host).await?;134		info!(135			"rollback target would be {} {}",136			generation.id, generation.datetime137		);138		{139			let mut cmd = host.cmd("sh").await?;140			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));141			if let Err(e) = cmd.sudo().run().await {142				error!("failed to set rollback marker: {e}");143				failed = true;144			}145		}146		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.147		// Kicking it on manually will work best.148		//149		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will150		// only allow one instance of it.151152		// TODO: We should also watch how this process is going.153		// After running this command, we have less than 3 minutes to deploy everything,154		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.155		// Anyway, reboot will still help in this case.156		if action.should_schedule_rollback_run() {157			let mut cmd = host.cmd("systemd-run").await?;158			cmd.comparg("--on-active", "3min")159				.comparg("--unit", "rollback-watchdog-run")160				.arg("systemctl")161				.arg("start")162				.arg("rollback-watchdog.service");163			if let Err(e) = cmd.sudo().run().await {164				error!("failed to schedule rollback run: {e}");165				failed = true;166			}167		}168	}169170	if action.should_switch_profile() && !failed {171		info!("switching generation");172		let mut cmd = host.cmd("nix-env").await?;173		cmd.comparg("--profile", "/nix/var/nix/profiles/system")174			.comparg("--set", &built);175		if let Err(e) = cmd.sudo().run().await {176			error!("failed to switch generation: {e}");177			failed = true;178		}179	}180181	// FIXME: Connection might be disconnected after activation run182183	if action.should_activate() && !failed {184		let _span = info_span!("activating").entered();185		info!("executing activation script");186		let mut switch_script = built.clone();187		switch_script.push("bin");188		switch_script.push("switch-to-configuration");189		let mut cmd = host.cmd(switch_script).in_current_span().await?;190		cmd.arg(action.name().expect("upload.should_activate == false"));191		if let Err(e) = cmd.sudo().run().in_current_span().await {192			error!("failed to activate: {e}");193			failed = true;194		}195	}196	if !disable_rollback {197		if failed {198			info!("executing rollback");199			if let Err(e) = host200				.systemctl_start("rollback-watchdog.service")201				.instrument(info_span!("rollback"))202				.await203			{204				error!("failed to trigger rollback: {e}")205			}206		} else {207			info!("trying to mark upgrade as successful");208			if let Err(e) = host209				.rm_file("/etc/fleet_rollback_marker", true)210				.in_current_span()211				.await212			{213				error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")214			}215		}216		info!("disarming watchdog, just in case");217		if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {218			// It is ok, if there was no reboot - then timer might not be running.219		}220		if action.should_schedule_rollback_run() {221			if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {222				error!("failed to disarm rollback run: {e}");223			}224		}225	} else if let Err(_e) = host226		.rm_file("/etc/fleet_rollback_marker", true)227		.in_current_span()228		.await229	{230		// Marker might not exist, yet better try to remove it.231	}232	Ok(())233}234235async fn build_task(config: Config, host: String, build_attr: &str) -> Result<PathBuf> {236	info!("building");237	let host = config.host(&host).await?;238	// let action = Action::from(self.subcommand.clone());239	let fleet_config = &config.config_field;240	let drv = nix_go!(241		fleet_config.hosts[{ &host.name }]242			.nixosSystem243			.config244			.system245			.build[{ build_attr }]246	);247	let outputs = drv.build().await.map_err(|e| {248			if build_attr == "sdImage" {249				info!("sd-image build failed");250				info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");251			}252			e253		})?;254	let out_output = outputs255		.get("out")256		.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;257258	Ok(out_output.clone())259}260261impl BuildSystems {262	pub async fn run(self, config: &Config) -> Result<()> {263		let hosts = config.list_hosts().await?;264		let set = LocalSet::new();265		let build_attr = self.build_attr.clone();266		for host in hosts.into_iter() {267			if config.should_skip(&host.name) {268				continue;269			}270			let config = config.clone();271			let span = info_span!("build", host = field::display(&host.name));272			let hostname = host.name;273			let build_attr = build_attr.clone();274			// FIXME: Since the introduction of better-nix-eval,275			// due to single repl used for builds, hosts are waiting for each other to build,276			// instead of building concurrently.277			//278			// Open multiple repls?279			//280			// Create build batcher, which will behave similar to golangs281			// WaitGroup, and start executing once all the build tasks are scheduled?282			// This also allows to cleanup build output, as there will be no longer283			// "waiting for remote machine" messages in the cases when one package is needed for284			// multiple hosts.285			set.spawn_local(286				(async move {287					let built = match build_task(config, hostname.clone(), &build_attr).await {288						Ok(path) => path,289						Err(e) => {290							error!("failed to deploy host: {}", e);291							return;292						}293					};294					// TODO: Handle error295					let mut out = current_dir().expect("cwd exists");296					out.push(format!("built-{}", hostname));297298					info!("linking iso image to {:?}", out);299					if let Err(e) = symlink(built, out) {300						error!("failed to symlink: {e}")301					}302				})303				.instrument(span),304			);305		}306		set.await;307		Ok(())308	}309}310311impl Deploy {312	pub async fn run(self, config: &Config) -> Result<()> {313		let hosts = config.list_hosts().await?;314		let set = LocalSet::new();315		for host in hosts.into_iter() {316			if config.should_skip(&host.name) {317				continue;318			}319			let config = config.clone();320			let span = info_span!("deploy", host = field::display(&host.name));321			let hostname = host.name.clone();322			// FIXME: Fix repl concurrency (see build-systems)323			set.spawn_local(324				(async move {325					let built = match build_task(config.clone(), hostname.clone(), "toplevel").await326					{327						Ok(path) => path,328						Err(e) => {329							error!("failed to deploy host: {}", e);330							return;331						}332					};333					if !config.is_local(&hostname) {334						info!("uploading system closure");335						{336							// TODO: Move to remote_derivation method.337							// Alternatively, nix store make-content-addressed can be used,338							// at least for the first deployment, to provide trusted store key.339							//340							// It is much slower, yet doesn't require root on the deployer machine.341							let mut sign = MyCommand::new("nix");342							// Private key for host machine is registered in nix-sign.nix343							sign.arg("store")344								.arg("sign")345								.comparg("--key-file", "/etc/nix/private-key")346								.arg("-r")347								.arg(&built);348							if let Err(e) = sign.sudo().run_nix().await {349								warn!("Failed to sign store paths: {e}");350							};351						}352						let mut tries = 0;353						loop {354							match host.remote_derivation(&built).await {355								Ok(remote) => {356									assert!(remote == built, "CA derivations aren't implemented");357									break;358								}359								Err(e) if tries < 3 => {360									tries += 1;361									warn!("copy failure ({}/3): {}", tries, e);362									sleep(Duration::from_millis(5000)).await;363								}364								Err(e) => {365									error!("upload failed: {e}");366									return;367								}368							}369						}370					}371					if let Err(e) =372						deploy_task(self.action, &host, built, self.disable_rollback).await373					{374						error!("activation failed: {e}");375					}376				})377				.instrument(span),378			);379		}380		set.await;381		Ok(())382	}383}
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -7,8 +7,6 @@
 use anyhow::{anyhow, bail, ensure, Context, Result};
 use chrono::{DateTime, Utc};
 use clap::Parser;
-use futures::StreamExt;
-use itertools::Itertools;
 use owo_colors::OwoColorize;
 use serde::Deserialize;
 use std::{
@@ -570,7 +568,7 @@
 					config.replace_shared(
 						name.to_owned(),
 						update_owner_set(
-							&name,
+							name,
 							config,
 							data,
 							secret,
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -14,7 +14,6 @@
 use openssh::SessionBuilder;
 use serde::de::DeserializeOwned;
 use tempfile::NamedTempFile;
-use tracing::instrument;
 
 use crate::{
 	better_nix_eval::{Field, NixSessionPool},
@@ -90,6 +89,7 @@
 		cmd.arg(path);
 		cmd.run_string().await
 	}
+	#[allow(dead_code)]
 	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {
 		let text = self.read_file_text(path).await?;
 		Ok(serde_json::from_str(&text)?)
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -12,14 +12,17 @@
 mod fleetdata;
 
 use std::ffi::OsString;
-use std::io::{stderr, stdout, Write};
 use std::process::exit;
 use std::time::Duration;
 
 use anyhow::{bail, Result};
 use clap::Parser;
 
-use cmds::{build_systems::BuildSystems, info::Info, secrets::Secret};
+use cmds::{
+	build_systems::{BuildSystems, Deploy},
+	info::Info,
+	secrets::Secret,
+};
 use futures::future::LocalBoxFuture;
 use futures::stream::FuturesUnordered;
 use futures::TryStreamExt;
@@ -73,6 +76,8 @@
 enum Opts {
 	/// Prepare systems for deployments
 	BuildSystems(BuildSystems),
+
+	Deploy(Deploy),
 	/// Secret management
 	#[clap(subcommand)]
 	Secret(Secret),
@@ -94,6 +99,7 @@
 async fn run_command(config: &Config, command: Opts) -> Result<()> {
 	match command {
 		Opts::BuildSystems(c) => c.run(config).await?,
+		Opts::Deploy(d) => d.run(config).await?,
 		Opts::Secret(s) => s.run(config).await?,
 		Opts::Info(i) => i.run(config).await?,
 		Opts::Prefetch(p) => p.run(config).await?,
modifiedcrates/better-command/src/handler.rsdiffbeforeafterboth
--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -165,7 +165,7 @@
 								drv = pkg;
 							}
 						}
-						// info!(target: "nix","copying {} {} -> {}", drv, from, to);
+						info!(target: "nix","copying {} {} -> {}", drv, from, to);
 						let span = info_span!("copy", from, to, drv);
 						span.pb_start();
 						self.spans.insert(id, span);
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -38,11 +38,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1703974965,
-        "narHash": "sha256-dvZjLuAcLnv25bqStTL2ZICC5YSs8aynF5amRM+I6UM=",
+        "lastModified": 1704409229,
+        "narHash": "sha256-Vc41cRJ3trOnocovLe0zZE35pK5Lfuo/zHk0xx3CNDY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "9f434bd436e2bb5615827469ed651e30c26daada",
+        "rev": "786f788914f2a6e94cedf361541894e972b8fd23",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1703902408,
-        "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
+        "lastModified": 1704075545,
+        "narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
+        "rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -29,7 +29,7 @@
         llvmPkgs = pkgs.buildPackages.llvmPackages_11;
         rust =
           (pkgs.rustChannelOf {
-            date = "2023-12-29";
+            date = "2024-01-01";
             channel = "nightly";
           })
           .default