omnyssh_core/ssh/client.rs
1//! SSH connection management.
2//!
3//! Connections delegated to the system SSH binary.
4//! Also provides russh-based client for live metrics.
5
6use serde::{Deserialize, Serialize};
7
8/// Indicates where a host entry originated.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
10#[serde(rename_all = "snake_case")]
11pub enum HostSource {
12 /// Imported from `~/.ssh/config` at startup.
13 SshConfig,
14 /// Added manually through the TUI form.
15 #[default]
16 Manual,
17}
18
19/// A single host entry used for SSH connections.
20///
21/// Populated either from `~/.ssh/config` (via the parser) or from
22/// `~/.config/omnyssh/hosts.toml` (manual entries added through the TUI).
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Host {
25 /// Display name / alias (e.g. `"web-prod-1"`).
26 pub name: String,
27 /// Hostname or IP address to connect to.
28 pub hostname: String,
29 /// SSH user. Defaults to `"root"` when not specified.
30 #[serde(default = "default_user")]
31 pub user: String,
32 /// SSH port. Defaults to `22` when not specified.
33 #[serde(default = "default_port")]
34 pub port: u16,
35 /// Path to the private key file (e.g. `~/.ssh/id_ed25519`).
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub identity_file: Option<String>,
38 /// Password for password-based authentication (not recommended, used for initial setup).
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub password: Option<String>,
41 /// ProxyJump host alias (for bastion / jump-host setups).
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub proxy_jump: Option<String>,
44 /// Organisational tags (e.g. `["production", "web"]`).
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub tags: Vec<String>,
47 /// Free-text notes about this host.
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub notes: Option<String>,
50 /// Where this entry came from.
51 #[serde(default)]
52 pub source: HostSource,
53 /// Original host name from `~/.ssh/config` if this host was renamed.
54 /// Used to prevent duplicate entries when a SSH-config host is renamed.
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub original_ssh_host: Option<String>,
57
58 // -----------------------------------------------------------------------
59 // Auto SSH Key Setup metadata
60 // -----------------------------------------------------------------------
61 /// Date when SSH key was configured by OmnySSH (ISO 8601 format).
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub key_setup_date: Option<String>,
64 /// Whether password authentication has been disabled on the server.
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub password_auth_disabled: Option<bool>,
67}
68
69fn default_user() -> String {
70 std::env::var("USER")
71 .or_else(|_| std::env::var("LOGNAME"))
72 .unwrap_or_else(|_| String::from("root"))
73}
74
75fn default_port() -> u16 {
76 22
77}
78
79impl Default for Host {
80 fn default() -> Self {
81 Self {
82 name: String::new(),
83 hostname: String::new(),
84 user: default_user(),
85 port: default_port(),
86 identity_file: None,
87 password: None,
88 proxy_jump: None,
89 tags: Vec::new(),
90 notes: None,
91 source: HostSource::default(),
92 original_ssh_host: None,
93 key_setup_date: None,
94 password_auth_disabled: None,
95 }
96 }
97}
98
99/// Runtime connection status for a host. Never serialised to disk.
100#[derive(Debug, Clone, Default, PartialEq)]
101pub enum ConnectionStatus {
102 /// No connection attempt has been made yet.
103 #[default]
104 Unknown,
105 /// A connection attempt is currently in progress.
106 Connecting,
107 /// The host is connected.
108 Connected,
109 /// The last connection attempt failed with the given message.
110 Failed(String),
111}