diff --git a/CHANGELOG.md b/CHANGELOG.md index bd00df0a..501c797d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Added +- Nh now supports the `--build-host` and `--target-host` cli arguments + - Nh now checks if the current Nix implementation has necessary experimental features enabled. In mainline Nix (CppNix, etc.) we check for `nix-command` and `flakes` being set. In Lix, we also use `repl-flake` as it is still diff --git a/src/commands.rs b/src/commands.rs index 4125c71a..122730b5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,6 +10,17 @@ use tracing::{debug, info}; use crate::installable::Installable; +fn ssh_wrap(cmd: Exec, ssh: Option<&str>) -> Exec { + if let Some(ssh) = ssh { + Exec::cmd("ssh") + .arg("-T") + .arg(ssh) + .stdin(cmd.to_cmdline_lossy().as_str()) + } else { + cmd + } +} + #[derive(Debug)] pub struct Command { dry: bool, @@ -17,6 +28,7 @@ pub struct Command { command: OsString, args: Vec, elevate: bool, + ssh: Option, } impl Command { @@ -27,6 +39,7 @@ impl Command { command: command.as_ref().to_os_string(), args: vec![], elevate: false, + ssh: None, } } @@ -40,6 +53,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 +112,9 @@ 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 = + ssh_wrap(cmd.stderr(Redirection::None), self.ssh.as_deref()).stdout(Redirection::None); if let Some(m) = &self.message { info!("{}", m); @@ -106,9 +124,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()?; } } @@ -141,6 +159,7 @@ pub struct Build { installable: Installable, extra_args: Vec, nom: bool, + builder: Option, } impl Build { @@ -150,6 +169,7 @@ impl Build { installable, extra_args: vec![], nom: false, + builder: None, } } @@ -168,6 +188,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, @@ -192,9 +217,15 @@ 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")] + } + None => vec![], + }) .args(&self.extra_args) - .stdout(Redirection::Pipe) .stderr(Redirection::Merge) + .stdout(Redirection::Pipe) | Exec::cmd("nom").args(&["--json"]) } .stdout(Redirection::None); @@ -205,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 416efc1c..af01557c 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -132,6 +132,14 @@ 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, + + /// 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 d717fe46..36b4af9a 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,11 @@ impl OsRebuildArgs { fn rebuild(self, variant: OsRebuildVariant) -> Result<()> { use OsRebuildVariant::*; + if self.build_host.is_some() || self.target_host.is_some() { + // if it fails its okay + let _ = ensure_ssh_key_login(); + } + let elevate = if self.bypass_root_check { warn!("Bypassing root check, now running nix as root"); false @@ -102,6 +108,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()?; @@ -121,14 +128,18 @@ 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")?; - 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,14 +157,28 @@ impl OsRebuildArgs { } } + 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()?; + }; + 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(self.target_host.clone()) .message("Activating configuration") .elevate(elevate) .run()?; @@ -163,7 +188,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(self.target_host.clone()) .run()?; // !! Use the base profile aka no spec-namespace @@ -171,9 +197,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(self.target_host.clone()) .elevate(elevate) .message("Adding configuration to bootloader") .run()?; diff --git a/src/util.rs b/src/util.rs index 1765087c..5a7aa4f5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; +use std::process::{Command as StdCommand, Stdio}; use std::str; use color_eyre::{eyre, Result}; @@ -41,6 +42,27 @@ pub fn get_nix_version() -> Result { Err(eyre::eyre!("Failed to extract version")) } +/// 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 StdCommand::new("ssh-add") + .arg("-L") + .stdout(Stdio::null()) + .status()? + .success() + { + return Ok(()); + } + StdCommand::new("ssh-add") + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + .wait()?; + Ok(()) +} + /// Determines if the Nix binary is actually Lix /// /// # Returns