From 902970c867fd62dcc2c35d6a30f21ecb6696fa01 Mon Sep 17 00:00:00 2001 From: Shion Tanaka Date: Wed, 10 Jun 2026 11:20:08 +0900 Subject: [PATCH] kit: extract CommonSshOptions into ssh_options.rs Extract CommonSshOptions and SshConnectionOptions into a dedicated cross-platform module. These types were embedded in ssh.rs alongside Linux-specific container SSH logic, making them unavailable to other backends. Assisted-by: Antigravity (Claude Opus 4.6) Signed-off-by: Shion Tanaka --- crates/kit/src/lib.rs | 1 + crates/kit/src/main.rs | 1 + crates/kit/src/ssh.rs | 135 +-------------------------- crates/kit/src/ssh_options.rs | 171 ++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 133 deletions(-) create mode 100644 crates/kit/src/ssh_options.rs diff --git a/crates/kit/src/lib.rs b/crates/kit/src/lib.rs index 279e5caa5..777f28548 100644 --- a/crates/kit/src/lib.rs +++ b/crates/kit/src/lib.rs @@ -2,6 +2,7 @@ pub mod cpio; pub mod qemu_img; +pub mod ssh_options; pub mod xml_utils; // Linux-only modules diff --git a/crates/kit/src/main.rs b/crates/kit/src/main.rs index de0a3107e..a593feddb 100644 --- a/crates/kit/src/main.rs +++ b/crates/kit/src/main.rs @@ -11,6 +11,7 @@ mod cpio; mod install_options; mod instancetypes; mod qemu_img; +mod ssh_options; mod xml_utils; // Linux-only modules diff --git a/crates/kit/src/ssh.rs b/crates/kit/src/ssh.rs index 2cdbbda7e..705e3d636 100644 --- a/crates/kit/src/ssh.rs +++ b/crates/kit/src/ssh.rs @@ -9,6 +9,8 @@ use tracing::debug; use crate::CONTAINER_STATEDIR; +pub use crate::ssh_options::{CommonSshOptions, SshConnectionOptions}; + /// Combine multiple command arguments into a properly escaped shell command string /// /// This is necessary because SSH protocol sends commands as strings, not argument arrays. @@ -227,101 +229,6 @@ pub fn connect_via_container(container_name: &str, args: Vec) -> Result< Ok(()) } -/// SSH connection configuration options -#[derive(Debug, Clone)] -pub struct SshConnectionOptions { - /// Common SSH options shared across implementations - pub common: CommonSshOptions, - /// Enable/disable TTY allocation (default: true) - pub allocate_tty: bool, - /// Suppress output to stdout/stderr (default: false) - pub suppress_output: bool, -} - -/// Common SSH options that can be shared between different SSH implementations -#[derive(Debug, Clone)] -pub struct CommonSshOptions { - /// Use strict host key checking - pub strict_host_keys: bool, - /// SSH connection timeout in seconds - pub connect_timeout: u32, - /// Server alive interval in seconds - pub server_alive_interval: u32, - /// SSH log level - pub log_level: String, - /// Additional SSH options as key-value pairs - pub extra_options: Vec<(String, String)>, -} - -impl Default for CommonSshOptions { - fn default() -> Self { - Self { - strict_host_keys: false, - connect_timeout: 1, - server_alive_interval: 60, - log_level: "ERROR".to_string(), - extra_options: vec![], - } - } -} - -impl CommonSshOptions { - /// Apply these options to an SSH command - pub fn apply_to_command(&self, cmd: &mut std::process::Command) { - // Basic security options - cmd.args(["-o", "IdentitiesOnly=yes"]); - cmd.args(["-o", "PasswordAuthentication=no"]); - cmd.args(["-o", "KbdInteractiveAuthentication=no"]); - cmd.args(["-o", "GSSAPIAuthentication=no"]); - - // Connection options - cmd.args(["-o", &format!("ConnectTimeout={}", self.connect_timeout)]); - cmd.args([ - "-o", - &format!("ServerAliveInterval={}", self.server_alive_interval), - ]); - cmd.args(["-o", &format!("LogLevel={}", self.log_level)]); - - // Host key checking - if !self.strict_host_keys { - cmd.args(["-o", "StrictHostKeyChecking=no"]); - cmd.args(["-o", "UserKnownHostsFile=/dev/null"]); - } - - // Add extra SSH options - for (key, value) in &self.extra_options { - cmd.args(["-o", &format!("{}={}", key, value)]); - } - } -} - -impl Default for SshConnectionOptions { - fn default() -> Self { - Self { - common: CommonSshOptions::default(), - allocate_tty: true, - suppress_output: false, - } - } -} - -impl SshConnectionOptions { - /// Create options suitable for quick connectivity tests (short timeout, no TTY) - pub fn for_connectivity_test() -> Self { - Self { - common: CommonSshOptions { - strict_host_keys: false, - connect_timeout: 2, - server_alive_interval: 60, - log_level: "ERROR".to_string(), - extra_options: vec![], - }, - allocate_tty: false, - suppress_output: true, - } - } -} - /// Verify that a container exists and is running fn verify_container_running(container_name: &str) -> Result<()> { let status = Command::new("podman") @@ -402,44 +309,6 @@ mod tests { assert_eq!(permissions.mode() & 0o777, 0o600); } - #[test] - fn test_ssh_connection_options() { - // Test default options - let default_opts = SshConnectionOptions::default(); - assert_eq!(default_opts.common.connect_timeout, 1); - assert!(default_opts.allocate_tty); - assert_eq!(default_opts.common.log_level, "ERROR"); - assert!(default_opts.common.extra_options.is_empty()); - assert!(!default_opts.suppress_output); - - // Test connectivity test options - let test_opts = SshConnectionOptions::for_connectivity_test(); - assert_eq!(test_opts.common.connect_timeout, 2); - assert!(!test_opts.allocate_tty); - assert_eq!(test_opts.common.log_level, "ERROR"); - assert!(test_opts.common.extra_options.is_empty()); - assert!(test_opts.suppress_output); - - // Test custom options - let mut custom_opts = SshConnectionOptions::default(); - custom_opts.common.connect_timeout = 10; - custom_opts.allocate_tty = false; - custom_opts.common.log_level = "DEBUG".to_string(); - custom_opts - .common - .extra_options - .push(("ServerAliveInterval".to_string(), "30".to_string())); - - assert_eq!(custom_opts.common.connect_timeout, 10); - assert!(!custom_opts.allocate_tty); - assert_eq!(custom_opts.common.log_level, "DEBUG"); - assert_eq!(custom_opts.common.extra_options.len(), 1); - assert_eq!( - custom_opts.common.extra_options[0], - ("ServerAliveInterval".to_string(), "30".to_string()) - ); - } - #[test] fn test_shell_escape_command() { // Single argument diff --git a/crates/kit/src/ssh_options.rs b/crates/kit/src/ssh_options.rs new file mode 100644 index 000000000..596eda40a --- /dev/null +++ b/crates/kit/src/ssh_options.rs @@ -0,0 +1,171 @@ +//! Cross-platform SSH option types shared between different backends. +//! +//! Extracted from ssh.rs to allow macOS and Windows backends to share +//! SSH option types without pulling in Linux-specific dependencies. + +/// Common SSH options that can be shared between different SSH implementations +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct CommonSshOptions { + /// Use strict host key checking + pub strict_host_keys: bool, + /// SSH connection timeout in seconds + pub connect_timeout: u32, + /// Server alive interval in seconds + pub server_alive_interval: u32, + /// SSH log level + pub log_level: String, + /// Additional SSH options as key-value pairs + pub extra_options: Vec<(String, String)>, +} + +impl Default for CommonSshOptions { + fn default() -> Self { + Self { + strict_host_keys: false, + connect_timeout: 1, + server_alive_interval: 60, + log_level: "ERROR".to_string(), + extra_options: vec![], + } + } +} + +impl CommonSshOptions { + /// Apply these options to an SSH command + #[allow(dead_code)] + pub fn apply_to_command(&self, cmd: &mut std::process::Command) { + // Basic security options + cmd.args(["-o", "IdentitiesOnly=yes"]); + cmd.args(["-o", "PasswordAuthentication=no"]); + cmd.args(["-o", "KbdInteractiveAuthentication=no"]); + cmd.args(["-o", "GSSAPIAuthentication=no"]); + + // Connection options + cmd.args(["-o", &format!("ConnectTimeout={}", self.connect_timeout)]); + cmd.args([ + "-o", + &format!("ServerAliveInterval={}", self.server_alive_interval), + ]); + cmd.args(["-o", &format!("LogLevel={}", self.log_level)]); + + // Host key checking + if !self.strict_host_keys { + cmd.args(["-o", "StrictHostKeyChecking=no"]); + cmd.args(["-o", "UserKnownHostsFile=/dev/null"]); + } + + // Add extra SSH options + for (key, value) in &self.extra_options { + cmd.args(["-o", &format!("{}={}", key, value)]); + } + } +} + +/// SSH connection configuration options +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct SshConnectionOptions { + /// Common SSH options shared across implementations + pub common: CommonSshOptions, + /// Enable/disable TTY allocation (default: true) + pub allocate_tty: bool, + /// Suppress output to stdout/stderr (default: false) + pub suppress_output: bool, +} + +impl Default for SshConnectionOptions { + fn default() -> Self { + Self { + common: CommonSshOptions::default(), + allocate_tty: true, + suppress_output: false, + } + } +} + +impl SshConnectionOptions { + /// Create options suitable for quick connectivity tests (short timeout, no TTY) + #[allow(dead_code)] + pub fn for_connectivity_test() -> Self { + Self { + common: CommonSshOptions { + strict_host_keys: false, + connect_timeout: 2, + server_alive_interval: 60, + log_level: "ERROR".to_string(), + extra_options: vec![], + }, + allocate_tty: false, + suppress_output: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_common_ssh_options_default() { + let opts = CommonSshOptions::default(); + assert!(!opts.strict_host_keys); + assert_eq!(opts.connect_timeout, 1); + assert_eq!(opts.server_alive_interval, 60); + assert_eq!(opts.log_level, "ERROR"); + assert!(opts.extra_options.is_empty()); + } + + #[test] + fn test_ssh_connection_options() { + // Test default options + let default_opts = SshConnectionOptions::default(); + assert_eq!(default_opts.common.connect_timeout, 1); + assert!(default_opts.allocate_tty); + assert_eq!(default_opts.common.log_level, "ERROR"); + assert!(default_opts.common.extra_options.is_empty()); + assert!(!default_opts.suppress_output); + + // Test connectivity test options + let test_opts = SshConnectionOptions::for_connectivity_test(); + assert_eq!(test_opts.common.connect_timeout, 2); + assert!(!test_opts.allocate_tty); + assert_eq!(test_opts.common.log_level, "ERROR"); + assert!(test_opts.common.extra_options.is_empty()); + assert!(test_opts.suppress_output); + + // Test custom options + let mut custom_opts = SshConnectionOptions::default(); + custom_opts.common.connect_timeout = 10; + custom_opts.allocate_tty = false; + custom_opts.common.log_level = "DEBUG".to_string(); + custom_opts + .common + .extra_options + .push(("ServerAliveInterval".to_string(), "30".to_string())); + + assert_eq!(custom_opts.common.connect_timeout, 10); + assert!(!custom_opts.allocate_tty); + assert_eq!(custom_opts.common.log_level, "DEBUG"); + assert_eq!(custom_opts.common.extra_options.len(), 1); + assert_eq!( + custom_opts.common.extra_options[0], + ("ServerAliveInterval".to_string(), "30".to_string()) + ); + } + + #[test] + fn test_apply_to_command() { + let opts = CommonSshOptions::default(); + let mut cmd = std::process::Command::new("ssh"); + opts.apply_to_command(&mut cmd); + let args: Vec<_> = cmd + .get_args() + .map(|a| a.to_string_lossy().to_string()) + .collect(); + assert!(args.contains(&"IdentitiesOnly=yes".to_string())); + assert!(args.contains(&"PasswordAuthentication=no".to_string())); + assert!(args.contains(&"StrictHostKeyChecking=no".to_string())); + assert!(args.contains(&"ConnectTimeout=1".to_string())); + } +}