diff options
Diffstat (limited to 'src/cli')
| -rw-r--r-- | src/cli/machine.rs | 113 | ||||
| -rw-r--r-- | src/cli/spec/machine.rs | 11 | ||||
| -rw-r--r-- | src/cli/target.rs | 2 |
3 files changed, 125 insertions, 1 deletions
diff --git a/src/cli/machine.rs b/src/cli/machine.rs index 53404c03..9328347e 100644 --- a/src/cli/machine.rs +++ b/src/cli/machine.rs @@ -4,6 +4,8 @@ use crate::client::endpoint::{EndpointCatalog, ProfileId}; const HELP: &str = "Usage: herdr machine list [--json] + herdr machine status [<label-or-id>] [--json] + herdr machine reconnect <label-or-id> herdr machine add <ssh-target> --label <label> [--remote-session <name>] herdr machine rename <profile-id> --label <label> herdr machine remove <profile-id> @@ -30,6 +32,8 @@ struct MachineListRow<'a> { pub(super) fn run_machine_command(args: &[String]) -> std::io::Result<i32> { match args.first().map(String::as_str) { Some("list") => list(&args[1..]), + Some("status") => status(&args[1..]), + Some("reconnect") => reconnect(&args[1..]), Some("add") => add(&args[1..]), Some("rename") => rename(&args[1..]), Some("remove") => remove(&args[1..]), @@ -89,6 +93,115 @@ fn list(args: &[String]) -> std::io::Result<i32> { Ok(0) } +#[derive(Serialize)] +struct MachineStatusRow<'a> { + id: &'a str, + label: &'a str, + status: &'static str, + error: Option<String>, +} + +fn status(args: &[String]) -> std::io::Result<i32> { + let mut json = false; + let mut selector = None; + for arg in args { + if arg == "--json" && !json { + json = true; + } else if !arg.starts_with('-') && selector.is_none() { + selector = Some(arg.as_str()); + } else { + eprintln!("usage: herdr machine status [<label-or-id>] [--json]"); + return Ok(2); + } + } + let catalog = load_catalog()?; + let profiles = match selector { + Some(selector) => match super::target::resolve_machine(&catalog.ssh, selector) { + Ok(profile) => vec![profile], + Err(error) => { + eprintln!("{error}"); + return Ok(2); + } + }, + None => catalog.ssh.iter().collect(), + }; + let rows = profiles + .into_iter() + .map(|profile| { + let (status, error) = if !profile.enabled { + ("disabled", None) + } else { + match crate::remote::check_saved_ssh(&profile.target, &profile.session) { + Ok(()) => ("reachable", None), + Err(error) => { + let message = error.to_string(); + let status = if crate::remote::ssh_error_requires_authentication(&message) { + "auth required" + } else { + "error" + }; + (status, Some(message)) + } + } + }; + MachineStatusRow { + id: profile.id.as_str(), + label: &profile.label, + status, + error, + } + }) + .collect::<Vec<_>>(); + if json { + println!( + "{}", + serde_json::to_string_pretty(&rows).map_err(std::io::Error::other)? + ); + } else { + for row in &rows { + println!("{}\t{}\t{}", row.id, row.label, row.status); + if let Some(error) = &row.error { + println!(" {}", error.escape_debug()); + } + } + if rows.is_empty() { + println!("No saved SSH machines."); + } + } + Ok(i32::from(rows.iter().any(|row| row.error.is_some()))) +} + +fn reconnect(args: &[String]) -> std::io::Result<i32> { + use std::io::IsTerminal; + let [selector] = args else { + eprintln!("usage: herdr machine reconnect <label-or-id>"); + return Ok(2); + }; + let catalog = load_catalog()?; + let profile = match super::target::resolve_machine(&catalog.ssh, selector) { + Ok(profile) => profile, + Err(error) => { + eprintln!("{error}"); + return Ok(2); + } + }; + if !std::io::stdin().is_terminal() { + eprintln!("reconnect requires an interactive terminal; use herdr machine status for noninteractive checks"); + return Ok(2); + } + let mut authentication = crate::remote::ssh_authentication_command(&profile.target)?; + if !authentication.command.status()?.success() { + eprintln!("SSH authentication failed; the saved machine was not changed."); + return Ok(1); + } + crate::remote::check_saved_ssh(&profile.target, &profile.session)?; + println!( + "Machine {} is reachable. Open Herdr clients retry within 30 seconds.", + profile.id + ); + Ok(0) +} + #[derive(Debug, PartialEq, Eq)] struct AddArgs { target: String, diff --git a/src/cli/spec/machine.rs b/src/cli/spec/machine.rs index 188e27b9..82bc4c5d 100644 --- a/src/cli/spec/machine.rs +++ b/src/cli/spec/machine.rs @@ -11,6 +11,17 @@ pub(super) fn command() -> Command { .arg(json_flag()), ) .subcommand( + Command::new("status") + .about("Check saved machines without prompting for authentication") + .arg(Arg::new("machine").value_name("LABEL_OR_ID")) + .arg(json_flag()), + ) + .subcommand( + Command::new("reconnect") + .about("Authenticate a saved machine in this terminal and verify connectivity") + .arg(Arg::new("machine").value_name("LABEL_OR_ID").required(true)), + ) + .subcommand( Command::new("add") .about("Prepare the remote Herdr server and save an SSH machine") .arg( diff --git a/src/cli/target.rs b/src/cli/target.rs index 220410f9..9c8b9c80 100644 --- a/src/cli/target.rs +++ b/src/cli/target.rs @@ -257,7 +257,7 @@ fn parse_machine_prefix(args: &[String]) -> Result<Option<(String, Vec<String>)> Ok(Some((machine, cleaned))) } -fn resolve_machine<'a>( +pub(super) fn resolve_machine<'a>( profiles: &'a [SavedSshEndpoint], selector: &str, ) -> Result<&'a SavedSshEndpoint, String> { |
