sn_testnet_deploy/
ssh.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use 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    /// The list of VMs that are routed through a gateway.
91    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    /// Set the list of VMs that are routed through a Full Cone NAT Gateway.
102    /// This updates all the copies of the `SshClient` that have been cloned.
103    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    /// Set the list of VMs that are routed through a Port Restricted Cone NAT Gateway.
143    /// This updates all the copies of the `SshClient` that have been cloned.
144    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    /// Set the list of VMs that are routed through a Symmetric NAT Gateway.
185    /// This updates all the copies of the `SshClient` that have been cloned.
186    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}