2 Commits

Author SHA1 Message Date
7a324a04da Format Rust code using rustfmt 2025-06-27 11:15:11 +00:00
94fd8535ca Improved logging and formatting. 2025-05-07 14:35:01 +03:00
3 changed files with 149 additions and 96 deletions

2
Cargo.lock generated
View File

@ -1111,7 +1111,7 @@ checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]] [[package]]
name = "rexec" name = "rexec"
version = "1.3.0" version = "1.3.1"
dependencies = [ dependencies = [
"brace-expand", "brace-expand",
"clap 4.3.4", "clap 4.3.4",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rexec" name = "rexec"
version = "1.3.0" version = "1.3.1"
readme = "https://github.com/house-of-vanity/rexec#readme" readme = "https://github.com/house-of-vanity/rexec#readme"
edition = "2021" edition = "2021"
description = "Parallel SSH executor" description = "Parallel SSH executor"

View File

@ -25,7 +25,7 @@ use regex::Regex;
#[command(author = "AB ab@hexor.ru", version, about = "Parallel SSH executor in Rust", long_about = None)] #[command(author = "AB ab@hexor.ru", version, about = "Parallel SSH executor in Rust", long_about = None)]
struct Args { struct Args {
/// Username for SSH connections (defaults to current system user) /// Username for SSH connections (defaults to current system user)
#[arg(short, long, default_value_t = whoami::username())] #[arg(short = 'u', short = 'l', long, default_value_t = whoami::username())]
username: String, username: String,
/// Flag to use known_hosts file for server discovery instead of pattern expansion /// Flag to use known_hosts file for server discovery instead of pattern expansion
@ -71,7 +71,7 @@ struct Args {
#[arg( #[arg(
long, long,
help = "Use embedded SSH client instead of system SSH. Does not support 'live output'.", help = "Use embedded SSH client instead of system SSH. Does not support 'live output'.",
default_value_t = false, default_value_t = false
)] )]
embedded_ssh: bool, embedded_ssh: bool,
} }
@ -156,7 +156,7 @@ fn shorten_hostname(hostname: &str, common_suffix: &Option<String>) -> String {
Some(suffix) if hostname.ends_with(suffix) => { Some(suffix) if hostname.ends_with(suffix) => {
let short_name = hostname[..hostname.len() - suffix.len()].to_string(); let short_name = hostname[..hostname.len() - suffix.len()].to_string();
format!("{}{}", short_name, "*") format!("{}{}", short_name, "*")
}, }
_ => hostname.to_string(), _ => hostname.to_string(),
} }
} }
@ -290,7 +290,12 @@ fn expand_string(s: &str) -> Vec<Host> {
/// ///
/// # Returns /// # Returns
/// * `Result<i32, String>` - Exit code on success or error message /// * `Result<i32, String>` - Exit code on success or error message
fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suffix: &Option<String>) -> Result<i32, String> { fn execute_ssh_command(
hostname: &str,
username: &str,
command: &str,
common_suffix: &Option<String>,
) -> Result<i32, String> {
let display_name = shorten_hostname(hostname, common_suffix); let display_name = shorten_hostname(hostname, common_suffix);
// Display execution start message with shortened hostname // Display execution start message with shortened hostname
@ -298,8 +303,11 @@ fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suf
// Build the SSH command with appropriate options // Build the SSH command with appropriate options
let mut ssh_cmd = Command::new("ssh"); let mut ssh_cmd = Command::new("ssh");
ssh_cmd.arg("-o").arg("StrictHostKeyChecking=no") ssh_cmd
.arg("-o").arg("BatchMode=yes") .arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("BatchMode=yes")
.arg(format!("{}@{}", username, hostname)) .arg(format!("{}@{}", username, hostname))
.arg(command) .arg(command)
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@ -316,11 +324,17 @@ fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suf
let display_name_stdout = display_name.clone(); let display_name_stdout = display_name.clone();
let stdout_thread = thread::spawn(move || { let stdout_thread = thread::spawn(move || {
let reader = BufReader::new(stdout); let reader = BufReader::new(stdout);
let prefix = format!("{}", "".green()); let prefix = format!("{}", "".green());
for line in reader.lines() { for line in reader.lines() {
match line { match line {
Ok(line) => println!("{} {} - {}", prefix, display_name_stdout.yellow(), line), Ok(line) => println!(
"{} {} {} {}",
prefix,
display_name_stdout.yellow(),
prefix,
line
),
Err(_) => break, Err(_) => break,
} }
} }
@ -335,7 +349,13 @@ fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suf
for line in reader.lines() { for line in reader.lines() {
match line { match line {
Ok(line) => println!("{} {} - {}", prefix, display_name_stderr.yellow(), line), Ok(line) => println!(
"{} {} {} {}",
prefix,
display_name_stderr.yellow(),
prefix,
line
),
Err(_) => break, Err(_) => break,
} }
} }
@ -360,7 +380,11 @@ fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suf
}; };
// Display completion message // Display completion message
println!("{} - COMPLETED (Exit code: [{}])", display_name.yellow().bold(), code_string); println!(
"{} - COMPLETED (Exit code: [{}])",
display_name.yellow().bold(),
code_string
);
Ok(exit_code) Ok(exit_code)
} }
@ -377,7 +401,14 @@ fn execute_ssh_command(hostname: &str, username: &str, command: &str, common_suf
/// * `parallel` - Maximum number of parallel connections /// * `parallel` - Maximum number of parallel connections
/// * `code_only` - Whether to display only exit codes /// * `code_only` - Whether to display only exit codes
/// * `common_suffix` - Optional common suffix for hostname display formatting /// * `common_suffix` - Optional common suffix for hostname display formatting
fn execute_with_massh(hosts: &[(String, IpAddr, usize)], username: &str, command: &str, parallel: i32, code_only: bool, common_suffix: &Option<String>) { fn execute_with_massh(
hosts: &[(String, IpAddr, usize)],
username: &str,
command: &str,
parallel: i32,
code_only: bool,
common_suffix: &Option<String>,
) {
// Create a lookup table for host data using IP addresses as keys // Create a lookup table for host data using IP addresses as keys
let mut hosts_and_ips: HashMap<IpAddr, (String, usize)> = HashMap::new(); let mut hosts_and_ips: HashMap<IpAddr, (String, usize)> = HashMap::new();
let mut massh_hosts: Vec<MasshHostConfig> = Vec::new(); let mut massh_hosts: Vec<MasshHostConfig> = Vec::new();
@ -589,8 +620,9 @@ fn main() {
// Results are stored with original indices to maintain order // Results are stored with original indices to maintain order
let resolved_ips_with_indices = Arc::new(Mutex::new(Vec::<(String, IpAddr, usize)>::new())); let resolved_ips_with_indices = Arc::new(Mutex::new(Vec::<(String, IpAddr, usize)>::new()));
host_with_indices.par_iter().for_each(|(host, idx)| { host_with_indices
match lookup_host(&host.name) { .par_iter()
.for_each(|(host, idx)| match lookup_host(&host.name) {
Ok(ips) if !ips.is_empty() => { Ok(ips) if !ips.is_empty() => {
let ip = ips[0]; let ip = ips[0];
let mut results = resolved_ips_with_indices.lock().unwrap(); let mut results = resolved_ips_with_indices.lock().unwrap();
@ -598,12 +630,19 @@ fn main() {
} }
Ok(_) => { Ok(_) => {
let mut results = resolved_ips_with_indices.lock().unwrap(); let mut results = resolved_ips_with_indices.lock().unwrap();
results.push((host.name.clone(), IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), *idx)); results.push((
host.name.clone(),
IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)),
*idx,
));
} }
Err(_) => { Err(_) => {
let mut results = resolved_ips_with_indices.lock().unwrap(); let mut results = resolved_ips_with_indices.lock().unwrap();
results.push((host.name.clone(), IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), *idx)); results.push((
} host.name.clone(),
IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)),
*idx,
));
} }
}); });
@ -633,12 +672,18 @@ fn main() {
} }
// Find common domain suffix to optimize display // Find common domain suffix to optimize display
let hostnames: Vec<String> = valid_hosts.iter().map(|(hostname, _, _)| hostname.clone()).collect(); let hostnames: Vec<String> = valid_hosts
.iter()
.map(|(hostname, _, _)| hostname.clone())
.collect();
let common_suffix = find_common_suffix(&hostnames); let common_suffix = find_common_suffix(&hostnames);
// Inform user about display optimization if common suffix found // Inform user about display optimization if common suffix found
if let Some(suffix) = &common_suffix { if let Some(suffix) = &common_suffix {
info!("Common domain suffix found: '{}' (will be displayed as '*')", suffix); info!(
"Common domain suffix found: '{}' (will be displayed as '*')",
suffix
);
} }
// Ask for confirmation before proceeding (unless --noconfirm is specified) // Ask for confirmation before proceeding (unless --noconfirm is specified)
@ -681,7 +726,8 @@ fn main() {
// Execute SSH command in a separate thread // Execute SSH command in a separate thread
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
match execute_ssh_command(&hostname, &username, &command, &common_suffix_clone) { match execute_ssh_command(&hostname, &username, &command, &common_suffix_clone)
{
Ok(_) => (), Ok(_) => (),
Err(e) => error!("Error executing command on {}: {}", hostname, e), Err(e) => error!("Error executing command on {}: {}", hostname, e),
} }
@ -699,6 +745,13 @@ fn main() {
} }
} else { } else {
// Use the embedded massh library implementation // Use the embedded massh library implementation
execute_with_massh(&valid_hosts, &args.username, &args.command, args.parallel, args.code, &common_suffix); execute_with_massh(
&valid_hosts,
&args.username,
&args.command,
args.parallel,
args.code,
&common_suffix,
);
} }
} }