aboutsummaryrefslogtreecommitdiff
path: root/src/cli
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/machine.rs113
-rw-r--r--src/cli/spec/machine.rs11
-rw-r--r--src/cli/target.rs2
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> {