ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

attempt to use SSH ControlMaster whenever possible

+119 -35
+1
CHANGELOG.md
··· 15 15 - `--path` now supports flakerefs (`github:foo/bar`, `git+file:///...`, 16 16 `https://.../main.tar.gz`, etc). 17 17 - `--flake` is now an alias for `--path`. 18 + - Wire will now attempt to use SSH `ControlMaster` by default. 18 19 19 20 ### Fixed 20 21
+1 -1
wire/lib/src/commands/common.rs
··· 38 38 39 39 let child = run_command_with_env( 40 40 &CommandArguments::new(command_string, modifiers, clobber_lock).nix(), 41 - HashMap::from([("NIX_SSHOPTS".into(), node.target.create_ssh_opts(modifiers))]), 41 + HashMap::from([("NIX_SSHOPTS".into(), node.target.create_ssh_opts(modifiers, false)?)]), 42 42 )?; 43 43 44 44 child
+2 -1
wire/lib/src/commands/interactive.rs
··· 390 390 modifiers: SubCommandModifiers, 391 391 ) -> Result<portable_pty::CommandBuilder, HiveLibError> { 392 392 let mut command = portable_pty::CommandBuilder::new("ssh"); 393 - command.args(target.create_ssh_args(modifiers, false)?); 393 + command.args(target.create_ssh_args(modifiers, false, false)?); 394 + command.arg(target.get_preferred_host()?.to_string()); 394 395 Ok(command) 395 396 } 396 397
+2 -1
wire/lib/src/commands/noninteractive.rs
··· 193 193 modifiers: SubCommandModifiers, 194 194 ) -> Result<Command, HiveLibError> { 195 195 let mut command = Command::new("ssh"); 196 - command.args(target.create_ssh_args(modifiers, true)?); 196 + command.args(target.create_ssh_args(modifiers, true, false)?); 197 + command.arg(target.get_preferred_host()?.to_string()); 197 198 198 199 Ok(command) 199 200 }
+61 -32
wire/lib/src/hive/node.rs
··· 6 6 use gethostname::gethostname; 7 7 use serde::{Deserialize, Serialize}; 8 8 use std::collections::HashMap; 9 + use std::env; 9 10 use std::fmt::Display; 11 + use std::io::ErrorKind; 12 + use std::path::PathBuf; 10 13 use std::sync::{Arc, Mutex}; 11 - use tracing::{Level, error, event, instrument, trace}; 14 + use tracing::{Level, error, event, instrument, trace, warn}; 12 15 13 16 use crate::SubCommandModifiers; 14 17 use crate::commands::{CommandArguments, WireCommandChip, run_command_with_env}; 15 18 use crate::errors::NetworkError; 16 19 use crate::hive::HiveLocation; 17 20 use crate::hive::steps::build::Build; 21 + use crate::hive::steps::cleanup::CleanUp; 18 22 use crate::hive::steps::evaluate::Evaluate; 19 23 use crate::hive::steps::keys::{Key, Keys, PushKeyAgent, UploadKeyAt}; 20 24 use crate::hive::steps::ping::Ping; ··· 23 27 use super::HiveLibError; 24 28 use super::steps::activate::SwitchToConfiguration; 25 29 30 + const CONTROL_PERSIST: &str = "600s"; 31 + 26 32 #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, derive_more::Display)] 27 33 pub struct Name(pub Arc<str>); 28 34 ··· 37 43 } 38 44 39 45 impl Target { 40 - pub fn create_ssh_opts(&self, modifiers: SubCommandModifiers) -> String { 41 - format!( 42 - "-p {} {} {}", 43 - self.port, 44 - if modifiers.ssh_accept_host { 45 - "-o StrictHostKeyChecking=no" 46 - } else { 47 - "-o StrictHostKeyChecking=yes" 48 - }, 49 - if modifiers.non_interactive { 50 - // make nix refuse to auth with interactivity 51 - "-o PasswordAuthentication=no -o KbdInteractiveAuthentication=no".to_string() 52 - } else { 53 - String::new() 54 - } 55 - ) 46 + #[instrument(ret(level = tracing::Level::DEBUG), skip_all)] 47 + pub fn create_ssh_opts(&self, modifiers: SubCommandModifiers, master: bool) -> Result<String, HiveLibError> { 48 + Ok(self.create_ssh_args(modifiers, false, master)?.join(" ")) 56 49 } 57 50 51 + #[instrument(ret(level = tracing::Level::DEBUG), skip_all)] 58 52 pub fn create_ssh_args( 59 53 &self, 60 54 modifiers: SubCommandModifiers, 61 - non_elevated_forced: bool, 55 + non_interactive_forced: bool, 56 + master: bool 62 57 ) -> Result<Vec<String>, HiveLibError> { 63 58 let mut vector = vec![ 64 59 "-l".to_string(), 65 60 self.user.to_string(), 66 - self.get_preferred_host()?.to_string(), 67 61 "-p".to_string(), 68 62 self.port.to_string(), 69 63 ]; 70 - 71 - vector.extend([ 72 - "-o".to_string(), 64 + let mut options = vec![ 73 65 format!( 74 - "StrictHostKeyChecking {}", 66 + "StrictHostKeyChecking={}", 75 67 if modifiers.ssh_accept_host { 76 68 "no" 77 69 } else { ··· 79 71 } 80 72 ) 81 73 .to_string(), 82 - ]); 74 + ]; 75 + 76 + if modifiers.non_interactive || non_interactive_forced { 77 + options.extend(["PasswordAuthentication=no".to_string()]); 78 + options.extend(["KbdInteractiveAuthentication=no".to_string()]); 79 + } 83 80 84 - if modifiers.non_interactive || non_elevated_forced { 85 - vector.extend(["-o".to_string(), "PasswordAuthentication=no".to_string()]); 86 - vector.extend([ 87 - "-o".to_string(), 88 - "KbdInteractiveAuthentication=no".to_string(), 81 + if let Some(control_path) = get_control_path() { 82 + options.extend([ 83 + format!("ControlMaster={}", if master { 84 + "yes" 85 + } else { 86 + "no" 87 + }), format!("ControlPath={control_path}"), format!("ControlPersist={CONTROL_PERSIST}") 89 88 ]); 90 89 } 91 90 91 + vector.push("-o".to_string()); 92 + vector.extend(options.into_iter().intersperse("-o".to_string())); 93 + 92 94 Ok(vector) 93 95 } 94 96 } 95 97 98 + fn get_control_path() -> Option<String> { 99 + if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") { 100 + let control_path = PathBuf::from(runtime_dir).join("wire"); 101 + 102 + match std::fs::create_dir(&control_path) { 103 + Err(err) if err.kind() != ErrorKind::AlreadyExists => { 104 + error!("not using `ControlMaster`, failed to create path {control_path:?}: {err:?}"); 105 + return None; 106 + } 107 + _ => (), 108 + } 109 + 110 + return Some(control_path.join("%C").display().to_string()); 111 + } 112 + 113 + warn!("XDG_RUNTIME_DIR could not be found, disabling SSH `ControlMaster`"); 114 + None 115 + } 116 + 96 117 #[cfg(test)] 97 118 impl Default for Target { 98 119 fn default() -> Self { ··· 208 229 &CommandArguments::new(command_string, modifiers, clobber_lock) 209 230 .nix() 210 231 .log_stdout(), 211 - HashMap::from([("NIX_SSHOPTS".into(), self.target.create_ssh_opts(modifiers))]), 232 + HashMap::from([("NIX_SSHOPTS".into(), self.target.create_ssh_opts(modifiers, true)?)]), 212 233 )?; 213 234 214 235 output.wait_till_success().await.map_err(|source| { ··· 294 315 Build, 295 316 PushBuildOutput, 296 317 SwitchToConfiguration, 318 + CleanUp 297 319 } 298 320 299 321 impl Display for Step { ··· 307 329 Self::Build(step) => step.fmt(f), 308 330 Self::PushBuildOutput(step) => step.fmt(f), 309 331 Self::SwitchToConfiguration(step) => step.fmt(f), 332 + Self::CleanUp(step) => step.fmt(f), 310 333 } 311 334 } 312 335 } ··· 336 359 Step::Keys(Keys { 337 360 filter: UploadKeyAt::PostActivation, 338 361 }), 362 + Step::CleanUp(CleanUp), 339 363 ], 340 364 context, 341 365 } ··· 446 470 Keys { 447 471 filter: UploadKeyAt::PostActivation 448 472 } 449 - .into() 473 + .into(), 474 + CleanUp.into() 450 475 ] 451 476 ); 452 477 } ··· 472 497 filter: UploadKeyAt::NoFilter 473 498 } 474 499 .into(), 500 + CleanUp.into() 475 501 ] 476 502 ); 477 503 } ··· 495 521 crate::hive::steps::evaluate::Evaluate.into(), 496 522 crate::hive::steps::build::Build.into(), 497 523 crate::hive::steps::push::PushBuildOutput.into(), 524 + CleanUp.into() 498 525 ] 499 526 ); 500 527 } ··· 517 544 Ping.into(), 518 545 crate::hive::steps::evaluate::Evaluate.into(), 519 546 crate::hive::steps::push::PushEvaluatedOutput.into(), 547 + CleanUp.into() 520 548 ] 521 549 ); 522 550 } ··· 549 577 Keys { 550 578 filter: UploadKeyAt::PostActivation 551 579 } 552 - .into() 580 + .into(), 581 + CleanUp.into() 553 582 ] 554 583 ); 555 584 }
+50
wire/lib/src/hive/steps/cleanup.rs
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + // Copyright 2024-2025 wire Contributors 3 + 4 + use std::{fmt::Display}; 5 + 6 + use tokio::process::Command; 7 + use tracing::error; 8 + 9 + use crate::{errors::HiveLibError, hive::node::{Context, ExecuteStep}}; 10 + 11 + #[derive(PartialEq, Debug)] 12 + pub(crate) struct CleanUp; 13 + 14 + impl Display for CleanUp { 15 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 + write!(f, "Clean up") 17 + } 18 + } 19 + 20 + impl ExecuteStep for CleanUp { 21 + fn should_execute(&self, _ctx: &Context) -> bool { 22 + true 23 + } 24 + 25 + async fn execute(&self,ctx: &mut Context<'_>) -> Result<(), HiveLibError> { 26 + let output = Command::new("ssh") 27 + .args(ctx.node.target.create_ssh_args(ctx.modifiers, true, false)?) 28 + .args([ 29 + "-O", 30 + "stop", 31 + ctx.node.target.get_preferred_host()? 32 + ]) 33 + .output() 34 + .await; 35 + 36 + // non failing error handling because the apply was successful until 37 + // this point. 38 + match output { 39 + Err(err) => { 40 + error!("failed to wind-down ControlMaster with `ssh -O stop`: {err}"); 41 + } 42 + Ok(std::process::Output { status, stderr, .. }) if !status.success() => { 43 + error!("failed to wind-down ControlMaster with `ssh -O stop`: {}", String::from_utf8_lossy(&stderr)); 44 + } 45 + Ok(_) => {} 46 + } 47 + 48 + Ok(()) 49 + } 50 + }
+1
wire/lib/src/hive/steps/mod.rs
··· 7 7 pub mod keys; 8 8 pub mod ping; 9 9 pub mod push; 10 + pub mod cleanup;
+1
wire/lib/src/lib.rs
··· 8 8 clippy::missing_panics_doc 9 9 )] 10 10 #![feature(assert_matches)] 11 + #![feature(iter_intersperse)] 11 12 12 13 use std::io::IsTerminal; 13 14