use crate::ui::Colorize;
use anyhow::Result;
use std::path::Path;
use std::sync::Arc;
use crate::commands::error_format::format_connection_error;
use crate::executor::{ExecutionResult, ParallelExecutor};
use crate::node::Node;
use crate::security::Password;
use crate::ssh::known_hosts::StrictHostKeyChecking;
use crate::ssh::tokio_client::SshConnectionConfigResolver;
use crate::ui::OutputFormatter;
pub const PING_SSH_LEVEL_FAILURE: i32 = 255;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PingOutcome {
pub total: usize,
pub succeeded: usize,
pub failed: usize,
}
impl PingOutcome {
pub fn from_results(results: &[ExecutionResult]) -> Self {
let succeeded = results.iter().filter(|r| r.is_success()).count();
Self {
total: results.len(),
succeeded,
failed: results.len() - succeeded,
}
}
pub fn exit_code(&self) -> i32 {
if self.succeeded == 0 {
PING_SSH_LEVEL_FAILURE
} else if self.failed > 0 {
1
} else {
0
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn ping_nodes(
nodes: Vec<Node>,
max_parallel: usize,
key_path: Option<&Path>,
strict_mode: StrictHostKeyChecking,
use_agent: bool,
use_password: bool,
#[cfg(target_os = "macos")] use_keychain: bool,
timeout: Option<u64>,
connect_timeout: Option<u64>,
jump_hosts: Option<String>,
ssh_password: Option<Arc<Password>>,
ssh_connection_config_resolver: SshConnectionConfigResolver,
) -> Result<PingOutcome> {
println!(
"{}",
OutputFormatter::format_command_header("ping", nodes.len())
);
let key_path = key_path.map(|p| p.to_string_lossy().to_string());
let ping_timeout = timeout;
let executor = ParallelExecutor::new_with_all_options(
nodes.clone(),
max_parallel,
key_path,
strict_mode,
use_agent,
use_password,
)
.with_timeout(ping_timeout)
.with_connect_timeout(connect_timeout)
.with_jump_hosts(jump_hosts)
.with_ssh_password(ssh_password)
.with_ssh_connection_config_resolver(ssh_connection_config_resolver);
#[cfg(target_os = "macos")]
let executor = executor.with_keychain(use_keychain);
let results = executor.execute("true").await?;
let outcome = PingOutcome::from_results(&results);
println!("\n{} {}\n", "▶".cyan(), "Connection Test Results".bold());
for result in &results {
if result.is_success() {
println!(
" {} {} - {}",
"●".green(),
result.node.to_string().bold(),
"Connected".green()
);
} else {
println!(
" {} {} - {}",
"●".red(),
result.node.to_string().bold(),
"Failed".red()
);
if let Err(e) = &result.result {
let error_chain = format_connection_error(e);
for (i, line) in error_chain.lines().enumerate() {
if i == 0 {
println!(" {} {}", "└".dimmed(), line.dimmed());
} else {
println!(" {}", line.dimmed());
}
}
}
}
}
println!(
"{}",
OutputFormatter::format_summary(outcome.total, outcome.succeeded, outcome.failed)
);
Ok(outcome)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::executor::ExitCodeStrategy;
use crate::ssh::client::CommandResult;
use anyhow::anyhow;
fn reachable(host: &str) -> ExecutionResult {
ExecutionResult {
node: Node::new(host.to_string(), 22, "user".to_string()),
result: Ok(CommandResult {
host: host.to_string(),
output: Vec::new(),
stderr: Vec::new(),
exit_status: 0,
}),
is_main_rank: false,
}
}
fn unreachable(host: &str) -> ExecutionResult {
ExecutionResult {
node: Node::new(host.to_string(), 22, "user".to_string()),
result: Err(anyhow!("Connection refused")),
is_main_rank: false,
}
}
#[test]
fn all_hosts_reachable_exits_zero() {
let outcome = PingOutcome::from_results(&[reachable("host1"), reachable("host2")]);
assert_eq!(outcome.succeeded, 2);
assert_eq!(outcome.failed, 0);
assert_eq!(outcome.exit_code(), 0);
}
#[test]
fn partial_failure_exits_one() {
let outcome = PingOutcome::from_results(&[
reachable("host1"),
unreachable("host2"),
reachable("host3"),
]);
assert_eq!(outcome.succeeded, 2);
assert_eq!(outcome.failed, 1);
assert_eq!(outcome.exit_code(), 1);
}
#[test]
fn total_failure_exits_255() {
let outcome = PingOutcome::from_results(&[unreachable("host1"), unreachable("host2")]);
assert_eq!(outcome.succeeded, 0);
assert_eq!(outcome.failed, 2);
assert_eq!(outcome.exit_code(), PING_SSH_LEVEL_FAILURE);
}
#[test]
fn empty_host_list_exits_255() {
let outcome = PingOutcome::from_results(&[]);
assert_eq!(outcome.total, 0);
assert_eq!(outcome.exit_code(), PING_SSH_LEVEL_FAILURE);
}
#[test]
fn matches_require_all_success_whenever_a_host_answered() {
let cases = vec![
vec![reachable("host1"), reachable("host2")],
vec![reachable("host1"), unreachable("host2")],
vec![unreachable("host1"), reachable("host2")],
];
for results in cases {
let outcome = PingOutcome::from_results(&results);
assert_eq!(
outcome.exit_code(),
ExitCodeStrategy::RequireAllSuccess.calculate(&results, None),
"ping must agree with RequireAllSuccess when at least one host answered"
);
}
}
}