diff --git a/flake.nix b/flake.nix index 03f4a253..ad56f97f 100644 --- a/flake.nix +++ b/flake.nix @@ -87,12 +87,54 @@ # work around https://github.com/NixOS/nixpkgs/issues/73404 cd /tmp - $PROFILE/bin/switch-to-configuration switch + STORE_ROOT="''${STORE_ROOT%'/'}" + + _SYSTEM="$STORE_ROOT/nix/var/nix/profiles/system" + _PROFILE="$STORE_ROOT/$PROFILE" + _SWITCH_COMMAND="$PROFILE/bin/switch-to-configuration switch" # always relative to root + _NIXOS_ENTER_COMMAND="nixos-enter --root $STORE_ROOT" + + _already_on_nixos() { return [[ ! -f "$STORE_ROOT/etc/NIXOS" ]]; } + _set_system_profile() { + if [[ "$STORE_ROOT" == "" ]] + then + nix-env -p "$_SYSTEM" --set "$_PROFILE" + else + nix-env --store "$STORE_ROOT" -p "$_SYSTEM" --set "$_PROFILE" + done + } + _ensure_fs_contract() { mkdir -m 0755 -p "$STORE_ROOT"/etc; touch "$STORE_ROOT"/etc/NIXOS; } + _insall_bootloader_and_switch() { + ln -sfn /proc/mounts "$STORE_ROOT"/etc/mtab # Grub needs an mtab. + if [[ "$STORE_ROOT" == "" ]] + then + NIXOS_INSTALL_BOOTLOADER=1 $_SWITCH_COMMAND + else + NIXOS_INSTALL_BOOTLOADER=1 $_NIXOS_ENTER_COMMAND -- $_SWITCH_COMMAND + done + } + _switch_configuration() { + if [[ "$STORE_ROOT" == "" ]] + then + $_SWITCH_COMMAND + else + $_NIXOS_ENTER_COMMAND -- $_SWITCH_COMMAND + done + } + + if _already_on_nixos + then + _switch_configuration + else + _set_system_profile + _ensure_fs_contract + _insall_bootloader_and_switch + done # https://github.com/serokell/deploy-rs/issues/31 ${with base.config.boot.loader; final.lib.optionalString systemd-boot.enable - "sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"} + "sed -i '/^default /d' $STORE_ROOT/${efi.efiSysMountPoint}/loader/loader.conf"} ''; home-manager = base: custom base.activationPackage "$PROFILE/activate"; diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 9cf88199..7cf21f23 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -34,6 +34,10 @@ struct Opts { #[clap(long)] log_dir: Option, + /// Can activate on a mounted store root on the machines that is different from '/' + #[clap(long)] + store_root: Option, + #[clap(subcommand)] subcmd: SubCommand, } @@ -114,75 +118,106 @@ pub enum DeactivateError { ReactivateExitError(Option), } -pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { +pub async fn deactivate( + store_root: Option<&str>, + profile_path: &str, +) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - - let nix_env_rollback_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--rollback") + let mut nix_env_rollback_cmd = Command::new("nix-env"); + if let Some(ref store_root) = store_root { + nix_env_rollback_cmd + .arg("--store") + .arg(store_root) + .arg("-p") + .arg(format!("{}/{}", &store_root, &profile_path)); + } else { + nix_env_rollback_cmd + .arg("-p") + .arg(&profile_path); + }; + nix_env_rollback_cmd + .arg("--rollback"); + let nix_env_rollback_exit_status = nix_env_rollback_cmd .status() .await .map_err(DeactivateError::RollbackError)?; - match nix_env_rollback_exit_status.code() { Some(0) => (), a => return Err(DeactivateError::RollbackExitError(a)), }; debug!("Listing generations"); - - let nix_env_list_generations_out = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--list-generations") + let mut nix_env_list_generations_cmd = Command::new("nix-env"); + if let Some(ref store_root) = store_root { + nix_env_list_generations_cmd + .arg("--store") + .arg(store_root) + .arg("-p") + .arg(format!("{}/{}", &store_root, &profile_path)); + } else { + nix_env_list_generations_cmd + .arg("-p") + .arg(&profile_path); + }; + nix_env_list_generations_cmd + .arg("--list-generations"); + let nix_env_list_generations_out = nix_env_list_generations_cmd .output() .await .map_err(DeactivateError::ListGenError)?; - match nix_env_list_generations_out.status.code() { Some(0) => (), a => return Err(DeactivateError::ListGenExitError(a)), }; let generations_list = String::from_utf8(nix_env_list_generations_out.stdout)?; - let last_generation_line = generations_list .lines() .last() .expect("Expected to find a generation in list"); - let last_generation_id = last_generation_line .split_whitespace() .next() .expect("Expected to get ID from generation entry"); - debug!("Removing generation entry {}", last_generation_line); warn!("Removing generation by ID {}", last_generation_id); - - let nix_env_delete_generation_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) + let mut nix_env_delete_generation_cmd = Command::new("nix-env"); + if let Some(ref store_root) = store_root { + nix_env_delete_generation_cmd + .arg("--store") + .arg(store_root) + .arg("-p") + .arg(format!("{}/{}", &store_root, &profile_path)); + } else { + nix_env_delete_generation_cmd + .arg("-p") + .arg(&profile_path); + }; + nix_env_delete_generation_cmd .arg("--delete-generations") - .arg(last_generation_id) + .arg(last_generation_id); + let nix_env_delete_generation_exit_status = nix_env_delete_generation_cmd .status() .await .map_err(DeactivateError::DeleteGenError)?; - match nix_env_delete_generation_exit_status.code() { Some(0) => (), a => return Err(DeactivateError::DeleteGenExitError(a)), }; info!("Attempting to re-activate the last generation"); - - let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) + let mut re_activate_cmd = Command::new(format!("{}/deploy-rs-activate", profile_path)); + if let Some(ref store_root) = store_root { + re_activate_cmd + .env("STORE_ROOT", store_root); + }; + re_activate_cmd .env("PROFILE", &profile_path) - .current_dir(&profile_path) + .current_dir(&profile_path); + let re_activate_exit_status = re_activate_cmd .status() .await .map_err(DeactivateError::ReactivateError)?; - match re_activate_exit_status.code() { Some(0) => (), a => return Err(DeactivateError::ReactivateExitError(a)), @@ -230,6 +265,7 @@ async fn danger_zone( } pub async fn activation_confirmation( + store_root: Option, profile_path: String, temp_path: String, confirm_timeout: u16, @@ -279,7 +315,10 @@ pub async fn activation_confirmation( if let Err(err) = danger_zone(done, confirm_timeout).await { error!("Error waiting for confirmation event: {}", err); - if let Err(err) = deactivate(&profile_path).await { + if let Err(err) = deactivate( + store_root.as_deref(), + &profile_path, + ).await { error!( "Error de-activating due to another error waiting for confirmation, oh no...: {}", err @@ -297,7 +336,10 @@ pub enum WaitError { #[error("Error waiting for activation: {0}")] Waiting(#[from] DangerZoneError), } -pub async fn wait(temp_path: String, closure: String) -> Result<(), WaitError> { +pub async fn wait( + temp_path: String, + closure: String +) -> Result<(), WaitError> { let lock_path = deploy::make_lock_path(&temp_path, &closure); let (created, done) = mpsc::channel(1); @@ -359,6 +401,7 @@ pub enum ActivateError { } pub async fn activate( + store_root: Option, profile_path: String, closure: String, auto_rollback: bool, @@ -368,12 +411,24 @@ pub async fn activate( dry_activate: bool, ) -> Result<(), ActivateError> { if !dry_activate { - info!("Activating profile"); - let nix_env_set_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) + let mut nix_env_set_cmd = Command::new("nix-env"); + + if let Some(ref store_root) = store_root { + nix_env_set_cmd + .arg("--store") + .arg(store_root) + .arg("-p") + .arg(format!("{}/{}", &store_root, &profile_path)); + } else { + nix_env_set_cmd + .arg("-p") + .arg(&profile_path); + }; + nix_env_set_cmd .arg("--set") - .arg(&closure) + .arg(&closure); + info!("Activating profile"); + let nix_env_set_exit_status = nix_env_set_cmd .status() .await .map_err(ActivateError::SetProfileError)?; @@ -381,7 +436,10 @@ pub async fn activate( Some(0) => (), a => { if auto_rollback && !dry_activate { - deactivate(&profile_path).await?; + deactivate( + store_root.as_deref(), + &profile_path, + ).await?; } return Err(ActivateError::SetProfileExitError(a)); } @@ -395,11 +453,18 @@ pub async fn activate( } else { &profile_path }; + let mut activate_cmd = Command::new(format!("{}/deploy-rs-activate", activation_location)); - let activate_status = match Command::new(format!("{}/deploy-rs-activate", activation_location)) + if let Some(ref store_root) = store_root { + activate_cmd + .env("STORE_ROOT", store_root); + }; + activate_cmd .env("PROFILE", activation_location) .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) - .current_dir(activation_location) + .current_dir(activation_location); + + let activate_status = match activate_cmd .status() .await .map_err(ActivateError::RunActivateError) @@ -407,7 +472,10 @@ pub async fn activate( Ok(x) => x, Err(e) => { if auto_rollback && !dry_activate { - deactivate(&profile_path).await?; + deactivate( + store_root.as_deref(), + &profile_path, + ).await?; } return Err(e); } @@ -418,7 +486,10 @@ pub async fn activate( Some(0) => (), a => { if auto_rollback { - deactivate(&profile_path).await?; + deactivate( + store_root.as_deref(), + &profile_path, + ).await?; } return Err(ActivateError::RunActivateExitError(a)); } @@ -431,12 +502,20 @@ pub async fn activate( if magic_rollback { info!("Magic rollback is enabled, setting up confirmation hook..."); - match activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure) - .await - { + match activation_confirmation( + store_root.clone(), + profile_path.clone(), + temp_path, + confirm_timeout, + closure + ) + .await { Ok(()) => {} Err(err) => { - deactivate(&profile_path).await?; + deactivate( + store_root.as_deref(), + &profile_path, + ).await?; return Err(ActivateError::ActivationConfirmationError(err)); } }; @@ -451,8 +530,14 @@ pub enum RevokeError { #[error("There was an error de-activating after an error was encountered: {0}")] DeactivateError(#[from] DeactivateError), } -async fn revoke(profile_path: String) -> Result<(), RevokeError> { - deactivate(profile_path.as_str()).await?; +async fn revoke( + store_root: Option, + profile_path: String, +) -> Result<(), RevokeError> { + deactivate( + store_root.as_deref(), + profile_path.as_str(), + ).await?; Ok(()) } @@ -480,6 +565,7 @@ async fn main() -> Result<(), Box> { let r = match opts.subcmd { SubCommand::Activate(activate_opts) => activate( + opts.store_root, activate_opts.profile_path, activate_opts.closure, activate_opts.auto_rollback, @@ -491,13 +577,19 @@ async fn main() -> Result<(), Box> { .await .map_err(|x| Box::new(x) as Box), - SubCommand::Wait(wait_opts) => wait(wait_opts.temp_path, wait_opts.closure) - .await - .map_err(|x| Box::new(x) as Box), + SubCommand::Wait(wait_opts) => wait( + wait_opts.temp_path, + wait_opts.closure, + ) + .await + .map_err(|x| Box::new(x) as Box), - SubCommand::Revoke(revoke_opts) => revoke(revoke_opts.profile_path) - .await - .map_err(|x| Box::new(x) as Box), + SubCommand::Revoke(revoke_opts) => revoke( + opts.store_root, + revoke_opts.profile_path, + ) + .await + .map_err(|x| Box::new(x) as Box), }; match r { diff --git a/src/data.rs b/src/data.rs index ad10cecf..24aa5823 100644 --- a/src/data.rs +++ b/src/data.rs @@ -318,6 +318,9 @@ pub struct Flags { /// Revoke all previously succeeded deploys when deploying multiple profiles #[clap(long)] pub rollback_succeeded: bool, + /// Install profile onto a mounted disk (bootstrap NixOS) + #[clap(long)] + pub store_root: Option, } impl<'a> DeployData<'a> { diff --git a/src/deploy.rs b/src/deploy.rs index 94cb1219..3d6cc4f2 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -31,6 +31,7 @@ impl<'a> SshCommand<'a> { } pub struct ActivateCommand<'a> { + store_root: Option<&'a str>, sudo: Option<&'a str>, profile_path: &'a str, temp_path: &'a str, @@ -46,6 +47,7 @@ pub struct ActivateCommand<'a> { impl<'a> ActivateCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Self { ActivateCommand { + store_root: d.flags.store_root.as_deref(), sudo: d.sudo.as_deref(), profile_path: &d.profile_path, temp_path: &d.temp_path, @@ -70,6 +72,10 @@ impl<'a> ActivateCommand<'a> { cmd = format!("{} --log-dir {}", cmd, log_dir); } + if let Some(store_root) = self.store_root { + cmd = format!("{} --store-root '{}'", cmd, store_root); + } + cmd = format!( "{} activate '{}' '{}' --temp-path '{}'", cmd, self.closure, self.profile_path, self.temp_path @@ -102,6 +108,7 @@ impl<'a> ActivateCommand<'a> { #[test] fn test_activation_command_builder() { + let store_root = Some("/mnt"); let sudo = Some("sudo -u test".to_string()); let profile_path = "/blah/profiles/test"; let closure = "/nix/store/blah/etc"; @@ -126,12 +133,13 @@ fn test_activation_command_builder() { log_dir, dry_activate }.build(), - "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback" + "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt --store-root '/mnt' activate '/nix/store/blah/etc' '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback" .to_string(), ); } pub struct WaitCommand<'a> { + store_root: Option<&'a str>, sudo: Option<&'a str>, closure: &'a str, temp_path: &'a str, @@ -142,6 +150,7 @@ pub struct WaitCommand<'a> { impl<'a> WaitCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Self { WaitCommand { + store_root: d.flags.store_root.as_deref(), sudo: d.sudo.as_deref(), temp_path: &d.temp_path, closure: &d.profile.profile_settings.path, @@ -161,6 +170,10 @@ impl<'a> WaitCommand<'a> { cmd = format!("{} --log-dir {}", cmd, log_dir); } + if let Some(store_root) = self.store_root { + cmd = format!("{} --store-root '{}'", cmd, store_root); + } + cmd = format!( "{} wait '{}' --temp-path '{}'", cmd, self.closure, self.temp_path, @@ -176,6 +189,7 @@ impl<'a> WaitCommand<'a> { #[test] fn test_wait_command_builder() { + let store_root = Some("/mnt"); let sudo = Some("sudo -u test".to_string()); let closure = "/nix/store/blah/etc"; let temp_path = "/tmp"; @@ -190,12 +204,13 @@ fn test_wait_command_builder() { debug_logs, log_dir }.build(), - "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt wait '/nix/store/blah/etc' --temp-path '/tmp'" + "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt --store-root '/mnt' wait '/nix/store/blah/etc' --temp-path '/tmp'" .to_string(), ); } pub struct RevokeCommand<'a> { + store_root: Option<&'a str>, sudo: Option<&'a str>, closure: &'a str, profile_path: &'a str, @@ -206,6 +221,7 @@ pub struct RevokeCommand<'a> { impl<'a> RevokeCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Self { RevokeCommand { + store_root: d.flags.store_root.as_deref(), sudo: d.sudo.as_deref(), profile_path: &d.profile_path, closure: &d.profile.profile_settings.path, @@ -226,6 +242,10 @@ impl<'a> RevokeCommand<'a> { cmd = format!("{} --log-dir {}", cmd, log_dir); } + if let Some(store_root) = self.store_root { + cmd = format!("{} --store-root '{}'", cmd, store_root); + } + cmd = format!("{} revoke '{}'", cmd, self.profile_path); if let Some(sudo_cmd) = &self.sudo { @@ -238,6 +258,7 @@ impl<'a> RevokeCommand<'a> { #[test] fn test_revoke_command_builder() { + let store_root = Some("/mnt"); let sudo = Some("sudo -u test".to_string()); let closure = "/nix/store/blah/etc"; let profile_path = "/nix/var/nix/per-user/user/profile"; @@ -252,7 +273,7 @@ fn test_revoke_command_builder() { debug_logs, log_dir }.build(), - "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt revoke '/nix/var/nix/per-user/user/profile'" + "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt --store-root '/mnt' revoke '/nix/var/nix/per-user/user/profile'" .to_string(), ); } diff --git a/src/push.rs b/src/push.rs index 7b85eb46..3d544077 100644 --- a/src/push.rs +++ b/src/push.rs @@ -100,18 +100,27 @@ pub struct CopyCommand<'a> { closure: &'a str, fast_connection: bool, check_sigs: &'a bool, - ssh_uri: &'a str, ssh_opts: String, + target: String, } impl<'a> CopyCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Self { + + let target = { + if let Some(ref store_root) = d.flags.store_root { + format!("{}:{}", &d.ssh_uri, store_root) + } else { + d.ssh_uri.to_owned() + } + }; + CopyCommand { closure: d.profile.profile_settings.path.as_str(), fast_connection: d.merged_settings.fast_connection.unwrap_or(false), check_sigs: &d.flags.checksigs, - ssh_uri: d.ssh_uri.as_str(), ssh_opts: d.merged_settings.ssh_opts.iter().fold("".to_string(), |s, o| format!("{} {}", s, o)), + target, } } @@ -129,7 +138,7 @@ impl<'a> CopyCommand<'a> { } cmd .arg("--to") - .arg(self.ssh_uri) + .arg(self.target) .arg(self.closure) .env("NIX_SSHOPTS", self.ssh_opts); //cmd.what_is_this; @@ -261,7 +270,7 @@ pub async fn push_profile( info!("Copying profile `{}` to node `{}`", profile_name, node_name); - let mut copy_cmd = copy.build(); + let mut copy_cmd = copy.build(); let copy_exit_status = copy_cmd .status()