git.delta.rocks / jrsonnet / refs/commits / 3627c6c6df00

difftreelog

feat nixos-install target

Lach2025-04-06parent: #3972fee.patch.diff
in: trunk

6 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -924,7 +924,6 @@
  "hostname",
  "human-repr",
  "indicatif",
- "indoc",
  "itertools 0.13.0",
  "nix-eval",
  "nixlike",
@@ -958,6 +957,7 @@
  "fleet-shared",
  "futures",
  "hostname",
+ "indoc",
  "itertools 0.13.0",
  "nix-eval",
  "nixlike",
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -47,7 +47,6 @@
 nix-eval.workspace = true
 nom = "7.1.3"
 fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
-indoc = "2.0.6"
 
 [features]
 default = ["indicatif"]
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, str::FromStr, time::Duration};1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, time::Duration};
22
3use anyhow::{anyhow, bail, Result};3use anyhow::{anyhow, bail, Result};
4use clap::{Parser, ValueEnum};4use clap::{Parser, ValueEnum};
5use fleet_base::{5use fleet_base::{
6 host::{Config, ConfigHost},6 host::{Config, ConfigHost, DeployKind},
7 opts::FleetOpts,7 opts::FleetOpts,
8};8};
9use itertools::Itertools as _;9use itertools::Itertools as _;
131 specialisation: Option<String>,131 specialisation: Option<String>,
132 disable_rollback: bool,132 disable_rollback: bool,
133) -> Result<()> {133) -> Result<()> {
134 let deploy_kind = host.deploy_kind().await?;
135 if deploy_kind == DeployKind::NixosInstall
136 && !matches!(action, DeployAction::Boot | DeployAction::Upload)
137 {
138 bail!("nixos-install deploy kind only supports boot and upload actions");
139 }
140
134 let mut failed = false;141 let mut failed = false;
135142
178 }185 }
179 }186 }
180
181 if action.should_switch_profile() && !failed {187 if deploy_kind == DeployKind::NixosInstall {
182 info!("switching system profile generation");188 info!(
183 // It would also be possible to update profile atomically during copy:189 "running nixos-install to switch profile, install bootloader, and perform activation"
184 // https://github.com/NixOS/nix/pull/11657190 );
185 let mut cmd = host.cmd("nix").await?;191 let mut cmd = host.cmd("nixos-install").await?;
186 cmd.arg("build");192 cmd.arg("--system").arg(&built).args([
187 cmd.comparg("--profile", "/nix/var/nix/profiles/system");193 // Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
188 cmd.arg(&built);194 // It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
189 if let Err(e) = cmd.sudo().run_nix().await {195 "--no-channel-copy",
190 error!("failed to switch system profile generation: {e}");196 "--root",
191 failed = true;197 "/mnt",
192 }198 ]);
199 if let Err(e) = cmd.sudo().run().await {
200 error!("failed to execute nixos-install: {e}");
201 failed = true;
202 }
193 }203 } else {
204 if action.should_switch_profile() && !failed {
205 info!("switching system profile generation");
206
207 // To avoid even more problems, using nixos-install for now.
208 // // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
209 // // falling back to using nix-env command
210 // // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
211 // let mut cmd = host.cmd("nix-env").await?;
212 // cmd.args([
213 // "--store",
214 // "/mnt",
215 // "--profile",
216 // "/mnt/nix/var/nix/profiles/system",
217 // "--set",
218 // ])
219 // .arg(&built);
220 // if let Err(e) = cmd.sudo().run_nix().await {
221 // error!("failed to switch system profile generation: {e}");
222 // failed = true;
223 // }
224 // It would also be possible to update profile atomically during copy:
225 // https://github.com/NixOS/nix/pull/11657
226 let mut cmd = host.nix_cmd().await?;
227 cmd.arg("build");
228 cmd.comparg("--profile", "/nix/var/nix/profiles/system");
229 cmd.arg(&built);
230 if let Err(e) = cmd.sudo().run_nix().await {
231 error!("failed to switch system profile generation: {e}");
232 failed = true;
233 }
234 }
194235
195 // FIXME: Connection might be disconnected after activation run236 // FIXME: Connection might be disconnected after activation run
196237
212 failed = true;253 failed = true;
213 }254 }
214 }255 }
256 }
215 if action.should_create_rollback_marker() {257 if action.should_create_rollback_marker() {
216 if !disable_rollback {258 if !disable_rollback {
217 if failed {259 if failed {
333 }375 }
334}376}
335
336#[derive(Clone, PartialEq, Copy)]
337enum DeployKind {
338 // NixOS => NixOS managed by fleet
339 UpgradeToFleet,
340 // NixOS managed by fleet => NixOS managed by fleet
341 Fleet,
342}
343impl FromStr for DeployKind {
344 type Err = anyhow::Error;
345 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
346 match s {
347 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),
348 "fleet" => Ok(Self::Fleet),
349 v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\""),
350 }
351 }
352}
353377
354impl Deploy {378impl Deploy {
355 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {379 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
367 let local_host = config.local_host();391 let local_host = config.local_host();
368 let opts = opts.clone();392 let opts = opts.clone();
369 let batch = batch.clone();393 let batch = batch.clone();
370 let mut deploy_kind: Option<DeployKind> =394 if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
371 opts.action_attr(&host, "deploy_kind").await?;395 host.set_deploy_kind(deploy_kind);
396 };
372397
373 set.spawn_local(398 set.spawn_local(
374 (async move {399 (async move {
382 }407 }
383 };408 };
409
384 if deploy_kind == None {410 let deploy_kind = match host.deploy_kind().await {
385 let is_fleet_managed = match host.file_exists("/etc/FLEET_HOST").await {
386 Ok(v) => v,411 Ok(v) => v,
387 Err(e) => {412 Err(e) => {
388 error!("failed to query remote system kind: {}", e);413 error!("failed to query target deploy kind: {e}");
389 return;414 return;
390 },415 }
391 };416 };
392 if !is_fleet_managed {
393 error!(indoc::indoc!{"
394 host is not marked as managed by fleet
395 if you're not trying to lustrate/install system from scratch,
396 you should either
397 1. manually create /etc/FLEET_HOST file on the target host,
398 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet
399 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos
400 "});
401 return;
402 }
403 deploy_kind = Some(DeployKind::Fleet);
404 }
405 let deploy_kind = deploy_kind.expect("deploy_kind is set");
406417
407 // TODO: Make disable_rollback a host attribute instead418 // TODO: Make disable_rollback a host attribute instead
408 let mut disable_rollback = self.disable_rollback;419 let mut disable_rollback = self.disable_rollback;
modifiedcrates/fleet-base/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-base/Cargo.toml
+++ b/crates/fleet-base/Cargo.toml
@@ -13,6 +13,7 @@
 fleet-shared.workspace = true
 futures = "0.3.30"
 hostname = "0.4.0"
+indoc = "2.0.6"
 itertools = "0.13.0"
 nix-eval.workspace = true
 nixlike.workspace = true
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -58,11 +58,35 @@
 	Su,
 }
 
+#[derive(Clone, PartialEq, Copy)]
+pub enum DeployKind {
+	/// NixOS => NixOS managed by fleet
+	UpgradeToFleet,
+	/// NixOS managed by fleet => NixOS managed by fleet
+	Fleet,
+	/// Remote host has /mnt, /mnt/boot mounted,
+	/// generated config is added to fleet configuration.
+	NixosInstall,
+}
+
+impl FromStr for DeployKind {
+	type Err = anyhow::Error;
+	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+		match s {
+			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),
+			"fleet" => Ok(Self::Fleet),
+			"nixos-install" => Ok(Self::NixosInstall),
+			v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\""),
+		}
+	}
+}
 pub struct ConfigHost {
 	config: Config,
 	pub name: String,
 	groups: OnceCell<Vec<String>>,
 
+	deploy_kind: OnceCell<DeployKind>,
+
 	pub host_config: Option<Value>,
 	pub nixos_config: OnceCell<Value>,
 	pub pkgs_override: Option<Value>,
@@ -73,6 +97,40 @@
 }
 // TODO: Move command helpers away with connectivity refactor
 impl ConfigHost {
+	pub fn set_deploy_kind(&self, kind: DeployKind) {
+		self.deploy_kind
+			.set(kind)
+			.ok()
+			.expect("deploy kind is already set");
+	}
+	pub async fn deploy_kind(&self) -> Result<DeployKind> {
+		if let Some(kind) = self.deploy_kind.get() {
+			return Ok(kind.clone());
+		}
+		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {
+			Ok(v) => v,
+			Err(e) => {
+				bail!("failed to query remote system kind: {}", e);
+			}
+		};
+		if !is_fleet_managed {
+			bail!(indoc::indoc! {"
+				host is not marked as managed by fleet
+				if you're not trying to lustrate/install system from scratch,
+				you should either
+					1. manually create /etc/FLEET_HOST file on the target host,
+					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet
+					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos
+			"});
+		}
+		// TOCTOU is possible
+		let _ = self.deploy_kind.set(DeployKind::Fleet);
+		Ok(self
+			.deploy_kind
+			.get()
+			.expect("deploy kind is just set")
+			.clone())
+	}
 	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {
 		// Prefer sudo, as run0 has some gotchas with polkit
 		// and too many repeating prompts.
@@ -189,6 +247,16 @@
 			Ok(MyCommand::new_on(escalation, cmd, session))
 		}
 	}
+	pub async fn nix_cmd(&self) -> Result<MyCommand> {
+		let mut nix = self.cmd("nix").await?;
+		nix.args([
+			"--extra-experimental-features",
+			"nix-command",
+			"--extra-experimental-features",
+			"flakes",
+		]);
+		Ok(nix)
+	}
 
 	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {
 		ensure!(data.encrypted, "secret is not encrypted");
@@ -231,10 +299,23 @@
 			EscalationStrategy::Su,
 			"nix",
 		);
-		nix.arg("copy")
-			.arg("--substitute-on-destination")
-			.comparg("--to", format!("ssh-ng://{}", self.name))
-			.arg(path);
+		nix.arg("copy").arg("--substitute-on-destination");
+
+		match self.deploy_kind().await? {
+			DeployKind::Fleet | DeployKind::UpgradeToFleet => {
+				nix.comparg("--to", format!("ssh-ng://{}", self.name));
+			}
+			DeployKind::NixosInstall => {
+				nix
+					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon
+					.arg("--no-check-sigs")
+					.comparg(
+						"--to",
+						format!("ssh-ng://root@{}-install?remote-store=/mnt", self.name),
+					);
+			}
+		}
+		nix.arg(path);
 		nix.run_nix().await.context("nix copy")?;
 		Ok(path.to_owned())
 	}
@@ -354,6 +435,7 @@
 
 			local: true,
 			session: OnceLock::new(),
+			deploy_kind: OnceCell::new(),
 		}
 	}
 
@@ -372,6 +454,7 @@
 			// TODO: Remove with connectivit refactor
 			local: self.localhost == name,
 			session: OnceLock::new(),
+			deploy_kind: OnceCell::new(),
 		})
 	}
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
modifiedmodules/nixos/meta.nixdiffbeforeafterboth
--- a/modules/nixos/meta.nix
+++ b/modules/nixos/meta.nix
@@ -13,5 +13,13 @@
   ];
 
   # Version of environment (fleet scripts such as rollback) already installed on the host
-  config.environment.etc.FLEET_HOST.text = "1";
+  config = {
+    environment.etc.FLEET_HOST.text = "1";
+
+    # Flake/nix command support is assumed by fleet, lets add it here to avoid potential problems.
+    nix.settings.experimental-features = [
+      "nix-command"
+      "flakes"
+    ];
+  };
 }