1use std::path::PathBuf;
6
7use clap::ValueEnum;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
11#[value(rename_all = "kebab-case")]
12pub enum HostKeyCheckMode {
13 Yes,
15 #[default]
17 AcceptNew,
18 No,
20}
21
22impl HostKeyCheckMode {
23 pub fn as_openssh_value(self) -> &'static str {
25 match self {
26 Self::Yes => "yes",
27 Self::AcceptNew => "accept-new",
28 Self::No => "no",
29 }
30 }
31}
32
33pub const TIMEOUT_KILL_AFTER_SECS: u64 = 2;
35
36pub const TIMEOUT_DETECTION_TIMEOUT_MS: u64 = 5000;
38
39#[derive(Debug, Clone)]
41pub struct SshConfig {
42 pub host: String,
44
45 pub port: u16,
47
48 pub username: String,
50
51 pub password: Option<String>,
53
54 pub private_key: Option<String>,
56
57 pub su_password: Option<String>,
59
60 pub sudo_password: Option<String>,
62
63 pub keepalive_interval: u64,
66
67 pub keepalive_max: u64,
70
71 pub max_output_tokens: Option<usize>,
74
75 pub reconnect_retries: u64,
77
78 pub reconnect_backoff_ms: u64,
80
81 pub health_probe_timeout_ms: u64,
83
84 pub host_key_checking: HostKeyCheckMode,
86
87 pub known_hosts: Option<PathBuf>,
89}
90
91impl SshConfig {
92 pub fn new(host: impl Into<String>, username: impl Into<String>) -> Self {
94 Self {
95 host: host.into(),
96 port: 22,
97 username: username.into(),
98 password: None,
99 private_key: None,
100 su_password: None,
101 sudo_password: None,
102 keepalive_interval: 30,
103 keepalive_max: 3,
104 max_output_tokens: Some(16_000),
105 reconnect_retries: 3,
106 reconnect_backoff_ms: 250,
107 health_probe_timeout_ms: 1500,
108 host_key_checking: HostKeyCheckMode::default(),
109 known_hosts: None,
110 }
111 }
112
113 pub fn with_port(mut self, port: u16) -> Self {
115 self.port = port;
116 self
117 }
118
119 pub fn with_password(mut self, password: impl Into<String>) -> Self {
121 self.password = Some(password.into());
122 self
123 }
124
125 pub fn with_private_key(mut self, key: impl Into<String>) -> Self {
127 self.private_key = Some(key.into());
128 self
129 }
130
131 pub fn with_su_password(mut self, password: impl Into<String>) -> Self {
133 self.su_password = Some(password.into());
134 self
135 }
136
137 pub fn with_sudo_password(mut self, password: impl Into<String>) -> Self {
139 self.sudo_password = Some(password.into());
140 self
141 }
142
143 pub fn with_keepalive_interval(mut self, secs: u64) -> Self {
147 self.keepalive_interval = secs;
148 self
149 }
150
151 pub fn with_keepalive_max(mut self, max: u64) -> Self {
155 self.keepalive_max = max;
156 self
157 }
158
159 pub fn with_max_output_tokens(mut self, tokens: Option<usize>) -> Self {
162 self.max_output_tokens = tokens;
163 self
164 }
165
166 pub fn with_reconnect_retries(mut self, retries: u64) -> Self {
168 self.reconnect_retries = retries;
169 self
170 }
171
172 pub fn with_reconnect_backoff_ms(mut self, backoff_ms: u64) -> Self {
174 self.reconnect_backoff_ms = backoff_ms;
175 self
176 }
177
178 pub fn with_health_probe_timeout_ms(mut self, timeout_ms: u64) -> Self {
180 self.health_probe_timeout_ms = timeout_ms;
181 self
182 }
183
184 pub fn with_host_key_checking(mut self, mode: HostKeyCheckMode) -> Self {
186 self.host_key_checking = mode;
187 self
188 }
189
190 pub fn with_known_hosts(mut self, known_hosts: Option<PathBuf>) -> Self {
192 self.known_hosts = known_hosts;
193 self
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_ssh_config_builder() {
203 let config = SshConfig::new("192.168.1.1", "admin")
204 .with_port(2222)
205 .with_password("secret")
206 .with_max_output_tokens(Some(5_000))
207 .with_reconnect_retries(4)
208 .with_reconnect_backoff_ms(500)
209 .with_health_probe_timeout_ms(1_200);
210
211 assert_eq!(config.host, "192.168.1.1");
212 assert_eq!(config.port, 2222);
213 assert_eq!(config.username, "admin");
214 assert_eq!(config.password, Some("secret".to_string()));
215 assert!(config.private_key.is_none());
216 assert_eq!(config.reconnect_retries, 4);
217 assert_eq!(config.reconnect_backoff_ms, 500);
218 assert_eq!(config.health_probe_timeout_ms, 1_200);
219 assert_eq!(config.host_key_checking, HostKeyCheckMode::AcceptNew);
220 assert!(config.known_hosts.is_none());
221 }
222}