1use crate::{
8 ansible::provisioning::PrivateNodeProvisionInventory,
9 error::{Error, Result},
10 inventory::VirtualMachine,
11 run_external_command,
12};
13use log::debug;
14use std::{
15 collections::HashMap,
16 net::IpAddr,
17 path::PathBuf,
18 sync::{Arc, RwLock},
19};
20
21#[derive(Clone, Debug)]
22pub struct RoutedVms {
23 full_cone_private_node_nat_gateway_ip_map: HashMap<VirtualMachine, IpAddr>,
24 port_restricted_cone_private_node_nat_gateway_ip_map: HashMap<VirtualMachine, IpAddr>,
25 symmetric_private_node_nat_gateway_ip_map: HashMap<VirtualMachine, IpAddr>,
26}
27
28impl RoutedVms {
29 pub fn find_symmetric_nat_routed_node(
30 &self,
31 ip_address: &IpAddr,
32 ) -> Option<(&VirtualMachine, &IpAddr)> {
33 debug!("Check if {ip_address} is a symmetric NAT routed node...");
34 self.symmetric_private_node_nat_gateway_ip_map
35 .iter()
36 .find_map(|(private_vm, gateway_ip)| {
37 if &private_vm.public_ip_addr == ip_address {
38 Some((private_vm, gateway_ip))
39 } else {
40 None
41 }
42 })
43 .inspect(|op| {
44 debug!("Found symmetric NAT routed node: {op:?}");
45 })
46 }
47
48 pub fn find_full_cone_nat_routed_node(
49 &self,
50 ip_address: &IpAddr,
51 ) -> Option<(&VirtualMachine, &IpAddr)> {
52 debug!("Check if {ip_address} is a full cone NAT routed node...");
53 self.full_cone_private_node_nat_gateway_ip_map
54 .iter()
55 .find_map(|(private_vm, gateway_ip)| {
56 if &private_vm.public_ip_addr == ip_address {
57 Some((private_vm, gateway_ip))
58 } else {
59 None
60 }
61 })
62 .inspect(|op| {
63 debug!("Found full cone NAT routed node: {op:?}");
64 })
65 }
66
67 pub fn find_port_restricted_cone_nat_routed_node(
68 &self,
69 ip_address: &IpAddr,
70 ) -> Option<(&VirtualMachine, &IpAddr)> {
71 debug!("Check if {ip_address} is a port restricted cone NAT routed node...");
72 self.port_restricted_cone_private_node_nat_gateway_ip_map
73 .iter()
74 .find_map(|(private_vm, gateway_ip)| {
75 if &private_vm.public_ip_addr == ip_address {
76 Some((private_vm, gateway_ip))
77 } else {
78 None
79 }
80 })
81 .inspect(|op| {
82 debug!("Found port restricted cone NAT routed node: {op:?}");
83 })
84 }
85}
86
87#[derive(Clone)]
88pub struct SshClient {
89 pub private_key_path: PathBuf,
90 pub routed_vms: Arc<RwLock<Option<RoutedVms>>>,
92}
93impl SshClient {
94 pub fn new(private_key_path: PathBuf) -> SshClient {
95 SshClient {
96 private_key_path,
97 routed_vms: Arc::new(RwLock::new(None)),
98 }
99 }
100
101 pub fn set_full_cone_nat_routed_vms(
104 &self,
105 private_node_vms: &[VirtualMachine],
106 nat_gateway_vms: &[VirtualMachine],
107 ) -> Result<()> {
108 let private_node_nat_gateway_map =
109 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
110 private_node_vms,
111 nat_gateway_vms,
112 )?;
113 let full_cone_private_node_nat_gateway_ip_map = private_node_nat_gateway_map
114 .into_iter()
115 .map(|(private_node_vm, nat_gateway_vm)| {
116 (private_node_vm, nat_gateway_vm.public_ip_addr)
117 })
118 .collect::<HashMap<_, _>>();
119 let mut write_access = self.routed_vms.write().map_err(|err| {
120 log::error!("Failed to set routed VMs: {err}");
121 Error::SshSettingsRwLockError
122 })?;
123
124 debug!("Full Cone Private Routed VMs have been set to: {full_cone_private_node_nat_gateway_ip_map:?}");
125 match write_access.as_mut() {
126 Some(routed_vms) => {
127 routed_vms.full_cone_private_node_nat_gateway_ip_map =
128 full_cone_private_node_nat_gateway_ip_map;
129 }
130 None => {
131 *write_access = Some(RoutedVms {
132 full_cone_private_node_nat_gateway_ip_map,
133 port_restricted_cone_private_node_nat_gateway_ip_map: HashMap::new(),
134 symmetric_private_node_nat_gateway_ip_map: HashMap::new(),
135 });
136 }
137 }
138
139 Ok(())
140 }
141
142 pub fn set_port_restricted_cone_nat_routed_vms(
145 &self,
146 private_node_vms: &[VirtualMachine],
147 nat_gateway_vms: &[VirtualMachine],
148 ) -> Result<()> {
149 let private_node_nat_gateway_map =
150 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
151 private_node_vms,
152 nat_gateway_vms,
153 )?;
154 let port_restricted_cone_private_node_nat_gateway_ip_map = private_node_nat_gateway_map
155 .into_iter()
156 .map(|(private_node_vm, nat_gateway_vm)| {
157 (private_node_vm, nat_gateway_vm.public_ip_addr)
158 })
159 .collect::<HashMap<_, _>>();
160 let mut write_access = self.routed_vms.write().map_err(|err| {
161 log::error!("Failed to set routed VMs: {err}");
162 Error::SshSettingsRwLockError
163 })?;
164
165 debug!("Port Restricted Cone Private node Routed VMs have been set to: {port_restricted_cone_private_node_nat_gateway_ip_map:?}");
166
167 match write_access.as_mut() {
168 Some(routed_vms) => {
169 routed_vms.port_restricted_cone_private_node_nat_gateway_ip_map =
170 port_restricted_cone_private_node_nat_gateway_ip_map;
171 }
172 None => {
173 *write_access = Some(RoutedVms {
174 full_cone_private_node_nat_gateway_ip_map: HashMap::new(),
175 port_restricted_cone_private_node_nat_gateway_ip_map,
176 symmetric_private_node_nat_gateway_ip_map: HashMap::new(),
177 });
178 }
179 }
180
181 Ok(())
182 }
183
184 pub fn set_symmetric_nat_routed_vms(
187 &self,
188 private_node_vms: &[VirtualMachine],
189 nat_gateway_vms: &[VirtualMachine],
190 ) -> Result<()> {
191 let private_node_nat_gateway_map =
192 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
193 private_node_vms,
194 nat_gateway_vms,
195 )?;
196 let symmetric_private_node_nat_gateway_ip_map = private_node_nat_gateway_map
197 .into_iter()
198 .map(|(private_node_vm, nat_gateway_vm)| {
199 (private_node_vm, nat_gateway_vm.public_ip_addr)
200 })
201 .collect::<HashMap<_, _>>();
202 let mut write_access = self.routed_vms.write().map_err(|err| {
203 log::error!("Failed to set routed VMs: {err}");
204 Error::SshSettingsRwLockError
205 })?;
206 debug!("Symmetric Private node Routed VMs have been set to: {symmetric_private_node_nat_gateway_ip_map:?}");
207
208 match write_access.as_mut() {
209 Some(routed_vms) => {
210 routed_vms.symmetric_private_node_nat_gateway_ip_map =
211 symmetric_private_node_nat_gateway_ip_map;
212 }
213 None => {
214 *write_access = Some(RoutedVms {
215 full_cone_private_node_nat_gateway_ip_map: HashMap::new(),
216 port_restricted_cone_private_node_nat_gateway_ip_map: HashMap::new(),
217 symmetric_private_node_nat_gateway_ip_map,
218 });
219 }
220 }
221
222 Ok(())
223 }
224
225 pub fn get_private_key_path(&self) -> PathBuf {
226 self.private_key_path.clone()
227 }
228
229 pub fn wait_for_ssh_availability(&self, ip_address: &IpAddr, user: &str) -> Result<()> {
230 let mut args = vec![
231 "-i".to_string(),
232 self.private_key_path.to_string_lossy().to_string(),
233 "-q".to_string(),
234 "-o".to_string(),
235 "BatchMode=yes".to_string(),
236 "-o".to_string(),
237 "ConnectTimeout=5".to_string(),
238 "-o".to_string(),
239 "StrictHostKeyChecking=no".to_string(),
240 ];
241 let routed_vm_read = self.routed_vms.read().map_err(|err| {
242 log::error!("Failed to read routed VMs: {err}");
243 Error::SshSettingsRwLockError
244 })?;
245 if let Some((vm, gateway_ip)) = routed_vm_read
246 .as_ref()
247 .and_then(|routed_vms| routed_vms.find_symmetric_nat_routed_node(ip_address))
248 {
249 println!(
250 "Checking for SSH availability at {} ({ip_address}) via symmetric NAT gateway {gateway_ip}...",
251 vm.private_ip_addr
252 );
253 debug!(
254 "Checking for SSH availability at {} ({ip_address}) via symmetric NAT gateway {gateway_ip}...",
255 vm.private_ip_addr
256 );
257 args.push("-o".to_string());
258 args.push(format!(
259 "ProxyCommand=ssh -i {} -W %h:%p {}@{}",
260 self.private_key_path.to_string_lossy(),
261 user,
262 gateway_ip
263 ));
264 args.push(format!("{user}@{}", vm.private_ip_addr));
265 } else if let Some((vm, gateway_ip)) = routed_vm_read
266 .as_ref()
267 .and_then(|routed_vms| routed_vms.find_full_cone_nat_routed_node(ip_address))
268 {
269 println!(
270 "Checking for SSH availability at {} ({ip_address}) via Full Cone NAT gateway {gateway_ip}...",
271 vm.private_ip_addr,
272 );
273 debug!(
274 "Checking for SSH availability at {} ({ip_address}) via Full Cone NAT gateway {gateway_ip}...",
275 vm.private_ip_addr,
276 );
277 args.push(format!("{user}@{gateway_ip}"));
278 } else if let Some((vm, gateway_ip)) = routed_vm_read
279 .as_ref()
280 .and_then(|routed_vms| routed_vms.find_port_restricted_cone_nat_routed_node(ip_address))
281 {
282 println!(
283 "Checking for SSH availability at {} ({ip_address}) via Port Restricted Cone NAT gateway {gateway_ip}...",
284 vm.private_ip_addr,
285 );
286 debug!(
287 "Checking for SSH availability at {} ({ip_address}) via Port Restricted Cone NAT gateway {gateway_ip}...",
288 vm.private_ip_addr,
289 );
290 args.push(format!("{user}@{gateway_ip}"));
291 } else {
292 println!("Checking for SSH availability at {ip_address}...");
293 args.push(format!("{user}@{ip_address}"));
294 }
295 args.push("bash".to_string());
296 args.push("--version".to_string());
297
298 let mut retries = 0;
299 let max_retries = 10;
300 while retries < max_retries {
301 let result = run_external_command(
302 PathBuf::from("ssh"),
303 std::env::current_dir()?,
304 args.clone(),
305 false,
306 false,
307 );
308 if result.is_ok() {
309 println!("SSH is available.");
310 return Ok(());
311 } else {
312 retries += 1;
313 println!("SSH is still unavailable after {retries} attempts.");
314 println!("Will sleep for 5 seconds then retry.");
315 std::thread::sleep(std::time::Duration::from_secs(5));
316 }
317 }
318
319 println!("The maximum number of connection retry attempts has been exceeded.");
320 Err(Error::SshUnavailable)
321 }
322
323 pub fn run_command(
324 &self,
325 ip_address: &IpAddr,
326 user: &str,
327 command: &str,
328 suppress_output: bool,
329 ) -> Result<Vec<String>> {
330 let command_args: Vec<String> = command.split_whitespace().map(String::from).collect();
331 let mut args = vec![
332 "-i".to_string(),
333 self.private_key_path.to_string_lossy().to_string(),
334 "-q".to_string(),
335 "-o".to_string(),
336 "BatchMode=yes".to_string(),
337 "-o".to_string(),
338 "ConnectTimeout=30".to_string(),
339 "-o".to_string(),
340 "StrictHostKeyChecking=no".to_string(),
341 ];
342 let routed_vm_read = self.routed_vms.read().map_err(|err| {
343 log::error!("Failed to read routed VMs: {err}");
344 Error::SshSettingsRwLockError
345 })?;
346
347 if let Some((vm, gateway)) = routed_vm_read
348 .as_ref()
349 .and_then(|routed_vms| routed_vms.find_symmetric_nat_routed_node(ip_address))
350 {
351 debug!(
352 "Running command '{}' on {} ({ip_address}) via symmetric NAT gateway {gateway}...",
353 command, vm.private_ip_addr
354 );
355 args.push("-o".to_string());
356 args.push(format!(
357 "ProxyCommand=ssh -i {} -W %h:%p {user}@{gateway}",
358 self.private_key_path.to_string_lossy(),
359 ));
360 args.push(format!("{user}@{}", vm.private_ip_addr));
361 } else if let Some((vm, gateway)) = routed_vm_read
362 .as_ref()
363 .and_then(|routed_vms| routed_vms.find_full_cone_nat_routed_node(ip_address))
364 {
365 debug!(
366 "Running command '{}' on {} ({ip_address}) via full cone NAT gateway {gateway}...",
367 command, vm.private_ip_addr
368 );
369 args.push(format!("{user}@{gateway}"));
370 } else if let Some((vm, gateway)) = routed_vm_read
371 .as_ref()
372 .and_then(|routed_vms| routed_vms.find_port_restricted_cone_nat_routed_node(ip_address))
373 {
374 debug!(
375 "Running command '{}' on {} ({ip_address}) via port restricted cone NAT gateway {gateway}...",
376 command, vm.private_ip_addr
377 );
378 args.push(format!("{user}@{gateway}"));
379 } else {
380 debug!("Running command '{command}' on {user}@{ip_address}...");
381 args.push(format!("{user}@{ip_address}"));
382 }
383 args.extend(command_args);
384
385 let output = run_external_command(
386 PathBuf::from("ssh"),
387 std::env::current_dir()?,
388 args,
389 suppress_output,
390 false,
391 )?;
392 Ok(output)
393 }
394
395 pub fn run_script(
396 &self,
397 ip_address: IpAddr,
398 user: &str,
399 script: PathBuf,
400 suppress_output: bool,
401 ) -> Result<Vec<String>> {
402 let file_name = script
403 .file_name()
404 .ok_or_else(|| {
405 Error::SshCommandFailed("Could not obtain file name from script path".to_string())
406 })?
407 .to_string_lossy()
408 .to_string();
409 let args = vec![
410 "-i".to_string(),
411 self.private_key_path.to_string_lossy().to_string(),
412 "-q".to_string(),
413 "-o".to_string(),
414 "BatchMode=yes".to_string(),
415 "-o".to_string(),
416 "ConnectTimeout=30".to_string(),
417 "-o".to_string(),
418 "StrictHostKeyChecking=no".to_string(),
419 script.to_string_lossy().to_string(),
420 format!("{}@{}:/tmp/{}", user, ip_address, file_name),
421 ];
422 run_external_command(
423 PathBuf::from("scp"),
424 std::env::current_dir()?,
425 args,
426 suppress_output,
427 false,
428 )
429 .map_err(|e| {
430 Error::SshCommandFailed(format!(
431 "Failed to copy script file to remote host {ip_address:?}: {e}"
432 ))
433 })?;
434
435 let args = vec![
436 "-i".to_string(),
437 self.private_key_path.to_string_lossy().to_string(),
438 "-q".to_string(),
439 "-o".to_string(),
440 "BatchMode=yes".to_string(),
441 "-o".to_string(),
442 "ConnectTimeout=30".to_string(),
443 "-o".to_string(),
444 "StrictHostKeyChecking=no".to_string(),
445 format!("{user}@{ip_address}"),
446 "bash".to_string(),
447 format!("/tmp/{file_name}"),
448 ];
449 let output = run_external_command(
450 PathBuf::from("ssh"),
451 std::env::current_dir()?,
452 args,
453 suppress_output,
454 false,
455 )
456 .map_err(|e| {
457 Error::SshCommandFailed(format!("Failed to execute command on remote host: {e}"))
458 })?;
459 Ok(output)
460 }
461}