From 477b751e35628165305d4571dff54e61add3af0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Mon, 5 May 2025 12:30:19 +0200 Subject: [PATCH 1/7] add --target_host option --- src/commands.rs | 24 +++++++++++++++++++----- src/interface.rs | 4 ++++ src/nixos.rs | 23 ++++++++++++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 4125c71a..191281a8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -17,6 +17,7 @@ pub struct Command { command: OsString, args: Vec, elevate: bool, + ssh: Option, } impl Command { @@ -27,6 +28,7 @@ impl Command { command: command.as_ref().to_os_string(), args: vec![], elevate: false, + ssh: None, } } @@ -40,6 +42,11 @@ impl Command { self } + pub fn ssh(mut self, ssh: Option) -> Self { + self.ssh = ssh; + self + } + pub fn arg>(mut self, arg: S) -> Self { self.args.push(arg.as_ref().to_os_string()); self @@ -94,9 +101,16 @@ impl Command { cmd.arg(&self.command).args(&self.args) } else { Exec::cmd(&self.command).args(&self.args) - } - .stderr(Redirection::None) - .stdout(Redirection::None); + }; + let cmd = if let Some(ssh) = &self.ssh { + Exec::cmd("ssh") + .arg("-T") + .arg(ssh) + .stdin(cmd.to_cmdline_lossy().as_str()) + } else { + cmd + }; + let cmd = cmd.stderr(Redirection::None).stdout(Redirection::None); if let Some(m) = &self.message { info!("{}", m); @@ -106,9 +120,9 @@ impl Command { if !self.dry { if let Some(m) = &self.message { - cmd.join().wrap_err(m.clone())?; + cmd.capture().wrap_err(m.clone())?; } else { - cmd.join()?; + cmd.capture()?; } } diff --git a/src/interface.rs b/src/interface.rs index 416efc1c..8c8465a7 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -132,6 +132,10 @@ pub struct OsRebuildArgs { /// Don't panic if calling nh as root #[arg(short = 'R', long, env = "NH_BYPASS_ROOT_CHECK")] pub bypass_root_check: bool, + + /// Deploy the configuration to a different host over ssh + #[arg(long)] + pub target_host: Option, } #[derive(Debug, Args)] diff --git a/src/nixos.rs b/src/nixos.rs index d717fe46..3c73098e 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -146,14 +146,31 @@ impl OsRebuildArgs { } } + let ssh = if let Some(target_host) = &self.target_host { + Command::new("nix") + .args([ + "copy", + "--to", + format!("ssh://{}", target_host).as_str(), + target_profile.to_str().unwrap(), + ]) + .message("Copying configuration to target") + .run()?; + Some(target_host) + } else { + None + }; + if let Test | Switch = variant { // !! Use the target profile aka spec-namespaced let switch_to_configuration = target_profile.join("bin").join("switch-to-configuration"); + let switch_to_configuration = switch_to_configuration.canonicalize().unwrap(); let switch_to_configuration = switch_to_configuration.to_str().unwrap(); Command::new(switch_to_configuration) .arg("test") + .ssh(ssh.cloned()) .message("Activating configuration") .elevate(elevate) .run()?; @@ -163,7 +180,8 @@ impl OsRebuildArgs { Command::new("nix") .elevate(elevate) .args(["build", "--no-link", "--profile", SYSTEM_PROFILE]) - .arg(out_path.get_path()) + .arg(out_path.get_path().canonicalize().unwrap()) + .ssh(ssh.cloned()) .run()?; // !! Use the base profile aka no spec-namespace @@ -171,9 +189,12 @@ impl OsRebuildArgs { .get_path() .join("bin") .join("switch-to-configuration"); + let switch_to_configuration = switch_to_configuration.canonicalize().unwrap(); + let switch_to_configuration = switch_to_configuration.to_str().unwrap(); Command::new(switch_to_configuration) .arg("boot") + .ssh(ssh.cloned()) .elevate(elevate) .message("Adding configuration to bootloader") .run()?; From 34e2aa5dd1923f49e4bfaff5e74cb80d58336b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Mon, 5 May 2025 15:22:23 +0200 Subject: [PATCH 2/7] build_host and changelog --- CHANGELOG.md | 6 ++++++ src/commands.rs | 45 +++++++++++++++++++++++++++++++-------------- src/interface.rs | 4 ++++ src/nixos.rs | 26 +++++++++++++------------- src/util.rs | 20 ++++++++++++++++++++ 5 files changed, 74 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd5059bc..7a493038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # NH Changelog +## Unreleased + +### Added + +- Nh now supports the `--build-host` and `--target-host` cli arguments + ## 4.0.3 ### Added diff --git a/src/commands.rs b/src/commands.rs index 191281a8..cdcf225d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,11 +4,21 @@ use color_eyre::{ eyre::{bail, Context}, Result, }; -use subprocess::{Exec, ExitStatus, Redirection}; +use subprocess::{Exec, ExitStatus, Pipeline, Redirection}; use thiserror::Error; use tracing::{debug, info}; -use crate::installable::Installable; +use crate::{installable::Installable, util::get_current_system}; + +// for some reason there isnt a common trait that both Exec and Pipeline implements, so just use +// the no op : from bash +fn ssh_wrap(cmd: Exec, ssh: Option<&str>) -> Pipeline { + if let Some(ssh) = ssh { + Exec::cmd("echo").arg(cmd.to_cmdline_lossy().as_str()) | Exec::cmd("ssh").arg("-T").arg(ssh) + } else { + Exec::cmd(":") | cmd + } +} #[derive(Debug)] pub struct Command { @@ -102,15 +112,8 @@ impl Command { } else { Exec::cmd(&self.command).args(&self.args) }; - let cmd = if let Some(ssh) = &self.ssh { - Exec::cmd("ssh") - .arg("-T") - .arg(ssh) - .stdin(cmd.to_cmdline_lossy().as_str()) - } else { - cmd - }; - let cmd = cmd.stderr(Redirection::None).stdout(Redirection::None); + let cmd = + ssh_wrap(cmd.stderr(Redirection::None), self.ssh.as_deref()).stdout(Redirection::None); if let Some(m) = &self.message { info!("{}", m); @@ -155,6 +158,7 @@ pub struct Build { installable: Installable, extra_args: Vec, nom: bool, + builder: Option, } impl Build { @@ -164,6 +168,7 @@ impl Build { installable, extra_args: vec![], nom: false, + builder: None, } } @@ -182,6 +187,11 @@ impl Build { self } + pub fn builder(mut self, builder: Option) -> Self { + self.builder = builder; + self + } + pub fn extra_args(mut self, args: I) -> Self where I: IntoIterator, @@ -206,9 +216,16 @@ impl Build { .arg("build") .args(&installable_args) .args(&["--log-format", "internal-json", "--verbose"]) + .args(&match &self.builder { + Some(host) => vec![ + "--builders".to_string(), + format!("ssh://{host} {} - - 100", get_current_system().unwrap()), + ], + None => vec![], + }) .args(&self.extra_args) - .stdout(Redirection::Pipe) .stderr(Redirection::Merge) + .stdout(Redirection::Pipe) | Exec::cmd("nom").args(&["--json"]) } .stdout(Redirection::None); @@ -219,8 +236,8 @@ impl Build { .arg("build") .args(&installable_args) .args(&self.extra_args) - .stdout(Redirection::None) - .stderr(Redirection::Merge); + .stderr(Redirection::Merge) + .stdout(Redirection::None); debug!(?cmd); cmd.join() diff --git a/src/interface.rs b/src/interface.rs index 8c8465a7..af01557c 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -136,6 +136,10 @@ pub struct OsRebuildArgs { /// Deploy the configuration to a different host over ssh #[arg(long)] pub target_host: Option, + + /// Build the configuration to a different host over ssh + #[arg(long)] + pub build_host: Option, } #[derive(Debug, Args)] diff --git a/src/nixos.rs b/src/nixos.rs index 3c73098e..f415acb1 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -102,6 +102,7 @@ impl OsRebuildArgs { .extra_arg("--out-link") .extra_arg(out_path.get_path()) .extra_args(&self.extra_args) + .builder(self.build_host.clone()) .message("Building NixOS configuration") .nom(!self.common.no_nom) .run()?; @@ -123,12 +124,14 @@ impl OsRebuildArgs { target_profile.try_exists().context("Doesn't exist")?; - Command::new("nvd") - .arg("diff") - .arg(CURRENT_PROFILE) - .arg(&target_profile) - .message("Comparing changes") - .run()?; + if self.build_host.is_none() && self.target_host.is_none() { + Command::new("nvd") + .arg("diff") + .arg(CURRENT_PROFILE) + .arg(&target_profile) + .message("Comparing changes") + .run()?; + } if self.common.dry || matches!(variant, Build) { if self.common.ask { @@ -146,7 +149,7 @@ impl OsRebuildArgs { } } - let ssh = if let Some(target_host) = &self.target_host { + if let Some(target_host) = &self.target_host { Command::new("nix") .args([ "copy", @@ -156,9 +159,6 @@ impl OsRebuildArgs { ]) .message("Copying configuration to target") .run()?; - Some(target_host) - } else { - None }; if let Test | Switch = variant { @@ -170,7 +170,7 @@ impl OsRebuildArgs { Command::new(switch_to_configuration) .arg("test") - .ssh(ssh.cloned()) + .ssh(self.target_host.clone()) .message("Activating configuration") .elevate(elevate) .run()?; @@ -181,7 +181,7 @@ impl OsRebuildArgs { .elevate(elevate) .args(["build", "--no-link", "--profile", SYSTEM_PROFILE]) .arg(out_path.get_path().canonicalize().unwrap()) - .ssh(ssh.cloned()) + .ssh(self.target_host.clone()) .run()?; // !! Use the base profile aka no spec-namespace @@ -194,7 +194,7 @@ impl OsRebuildArgs { Command::new(switch_to_configuration) .arg("boot") - .ssh(ssh.cloned()) + .ssh(self.target_host.clone()) .elevate(elevate) .message("Adding configuration to bootloader") .run()?; diff --git a/src/util.rs b/src/util.rs index 7ab460e0..9ad6808e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -59,6 +59,26 @@ pub fn get_nix_version() -> Result { Err(eyre::eyre!("Failed to extract version")) } +/// Retrieves the current system we're running on in the format nix expects +/// +/// This functions just runs `nix eval --impure --raw --expr 'builtins.currentSystem'` and gets the +/// output +/// +/// * `Result` - The current system string or an error if the version cannot be retrieved. +pub fn get_current_system() -> Result { + let output = Command::new("nix") + .args([ + "eval", + "--impure", + "--raw", + "--expr", + "builtins.currentSystem", + ]) + .output()?; + let output_str = str::from_utf8(&output.stdout)?; + Ok(output_str.to_string()) +} + pub trait MaybeTempPath: std::fmt::Debug { fn get_path(&self) -> &Path; } From 20219add8b5de3ffcce7111c7eca9855c9987595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Mon, 5 May 2025 16:43:12 +0200 Subject: [PATCH 3/7] using the no-op on nvd broke things --- src/commands.rs | 13 +++++++------ src/nixos.rs | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index cdcf225d..0789cc77 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,19 +4,20 @@ use color_eyre::{ eyre::{bail, Context}, Result, }; -use subprocess::{Exec, ExitStatus, Pipeline, Redirection}; +use subprocess::{Exec, ExitStatus, Redirection}; use thiserror::Error; use tracing::{debug, info}; use crate::{installable::Installable, util::get_current_system}; -// for some reason there isnt a common trait that both Exec and Pipeline implements, so just use -// the no op : from bash -fn ssh_wrap(cmd: Exec, ssh: Option<&str>) -> Pipeline { +fn ssh_wrap(cmd: Exec, ssh: Option<&str>) -> Exec { if let Some(ssh) = ssh { - Exec::cmd("echo").arg(cmd.to_cmdline_lossy().as_str()) | Exec::cmd("ssh").arg("-T").arg(ssh) + Exec::cmd("ssh") + .arg("-T") + .arg(ssh) + .stdin(cmd.to_cmdline_lossy().as_str()) } else { - Exec::cmd(":") | cmd + cmd } } diff --git a/src/nixos.rs b/src/nixos.rs index f415acb1..67c9831b 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -122,6 +122,8 @@ impl OsRebuildArgs { Some(spec) => out_path.get_path().join("specialisation").join(spec), }; + debug!("exists: {}", target_profile.exists()); + target_profile.try_exists().context("Doesn't exist")?; if self.build_host.is_none() && self.target_host.is_none() { From e7eebb59a10df6400925c9b7fb9251491c3de864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Mon, 5 May 2025 16:50:47 +0200 Subject: [PATCH 4/7] ensure that user has working ssh key in agent --- src/nixos.rs | 5 +++++ src/util.rs | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/nixos.rs b/src/nixos.rs index 67c9831b..7cdb50e6 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -13,6 +13,7 @@ use crate::installable::Installable; use crate::interface::OsSubcommand::{self}; use crate::interface::{self, OsGenerationsArgs, OsRebuildArgs, OsReplArgs}; use crate::update::update; +use crate::util::ensure_ssh_key_login; use crate::util::get_hostname; const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; @@ -51,6 +52,10 @@ impl OsRebuildArgs { fn rebuild(self, variant: OsRebuildVariant) -> Result<()> { use OsRebuildVariant::*; + if self.build_host.is_some() || self.target_host.is_some() { + ensure_ssh_key_login().unwrap(); + } + let elevate = if self.bypass_root_check { warn!("Bypassing root check, now running nix as root"); false diff --git a/src/util.rs b/src/util.rs index 9ad6808e..fce7a32d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ extern crate semver; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::str; use color_eyre::{eyre, Result}; @@ -79,6 +79,27 @@ pub fn get_current_system() -> Result { Ok(output_str.to_string()) } +/// Prompts the user for ssh key login if needed +pub fn ensure_ssh_key_login() -> Result<()> { + // ssh-add -L checks if there are any currently usable ssh keys + + if Command::new("ssh-add") + .arg("-L") + .stdout(Stdio::null()) + .status()? + .success() + { + return Ok(()); + } + Command::new("ssh-add") + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + .wait()?; + Ok(()) +} + pub trait MaybeTempPath: std::fmt::Debug { fn get_path(&self) -> &Path; } From 280e8524b3faceb46d2e93302224c9629dd67610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Wed, 7 May 2025 15:57:17 +0200 Subject: [PATCH 5/7] no need to fetch currentSystem and dont error if ssh-add isnt there --- src/commands.rs | 7 +++---- src/nixos.rs | 3 ++- src/util.rs | 20 -------------------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0789cc77..b30c790f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -218,10 +218,9 @@ impl Build { .args(&installable_args) .args(&["--log-format", "internal-json", "--verbose"]) .args(&match &self.builder { - Some(host) => vec![ - "--builders".to_string(), - format!("ssh://{host} {} - - 100", get_current_system().unwrap()), - ], + Some(host) => { + vec!["--builders".to_string(), format!("ssh://{host} - - - 100")] + } None => vec![], }) .args(&self.extra_args) diff --git a/src/nixos.rs b/src/nixos.rs index 7cdb50e6..36b4af9a 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -53,7 +53,8 @@ impl OsRebuildArgs { use OsRebuildVariant::*; if self.build_host.is_some() || self.target_host.is_some() { - ensure_ssh_key_login().unwrap(); + // if it fails its okay + let _ = ensure_ssh_key_login(); } let elevate = if self.bypass_root_check { diff --git a/src/util.rs b/src/util.rs index fce7a32d..527a96d0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -59,26 +59,6 @@ pub fn get_nix_version() -> Result { Err(eyre::eyre!("Failed to extract version")) } -/// Retrieves the current system we're running on in the format nix expects -/// -/// This functions just runs `nix eval --impure --raw --expr 'builtins.currentSystem'` and gets the -/// output -/// -/// * `Result` - The current system string or an error if the version cannot be retrieved. -pub fn get_current_system() -> Result { - let output = Command::new("nix") - .args([ - "eval", - "--impure", - "--raw", - "--expr", - "builtins.currentSystem", - ]) - .output()?; - let output_str = str::from_utf8(&output.stdout)?; - Ok(output_str.to_string()) -} - /// Prompts the user for ssh key login if needed pub fn ensure_ssh_key_login() -> Result<()> { // ssh-add -L checks if there are any currently usable ssh keys From 067380df4a803e3b23d8199e199fe266ce5aa247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Wed, 7 May 2025 16:02:46 +0200 Subject: [PATCH 6/7] delete unused import --- src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index b30c790f..122730b5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,7 +8,7 @@ use subprocess::{Exec, ExitStatus, Redirection}; use thiserror::Error; use tracing::{debug, info}; -use crate::{installable::Installable, util::get_current_system}; +use crate::installable::Installable; fn ssh_wrap(cmd: Exec, ssh: Option<&str>) -> Exec { if let Some(ssh) = ssh { From 75e09d07c56981db8bc0abf8a8b7789247cfb030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Sat, 10 May 2025 12:41:14 +0200 Subject: [PATCH 7/7] alias std command as StdCommand to not cause namespace clashes --- src/util.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util.rs b/src/util.rs index b7cdefde..5a7aa4f5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Command as StdCommand, Stdio}; use std::str; use color_eyre::{eyre, Result}; @@ -46,7 +46,7 @@ pub fn get_nix_version() -> Result { pub fn ensure_ssh_key_login() -> Result<()> { // ssh-add -L checks if there are any currently usable ssh keys - if Command::new("ssh-add") + if StdCommand::new("ssh-add") .arg("-L") .stdout(Stdio::null()) .status()? @@ -54,7 +54,7 @@ pub fn ensure_ssh_key_login() -> Result<()> { { return Ok(()); } - Command::new("ssh-add") + StdCommand::new("ssh-add") .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit())