ssh_scp_manager/ssh/
aws.rs

1use std::{
2    fs::{self, File},
3    io::{self, Write},
4    path::Path,
5};
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
10pub struct Command {
11    pub ssh_key_path: String,
12    pub user_name: String,
13
14    pub region: String,
15    pub availability_zone: String,
16
17    pub instance_id: String,
18    pub instance_state: String,
19
20    pub ip_mode: String,
21    pub public_ip: String,
22
23    pub profile: Option<String>,
24}
25
26/// ref. <https://doc.rust-lang.org/std/string/trait.ToString.html>
27/// ref. <https://doc.rust-lang.org/std/fmt/trait.Display.html>
28/// Use "Self.to_string()" to directly invoke this.
29impl std::fmt::Display for Command {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        // ssh -o "StrictHostKeyChecking no" -i [ssh_key_path] [user name]@[public IPv4/DNS name]
32        // aws ssm start-session --region [region] --target [instance ID]
33        write!(
34            f,
35            "# change SSH key permission
36chmod 400 {ssh_key_path}
37
38# instance '{instance_id}' ({instance_state}, {availability_zone}) -- ip mode '{ip_mode}'
39ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip}
40ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'tail -10 /var/log/cloud-init-output.log'
41ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'tail -f /var/log/cloud-init-output.log'
42
43# download a remote file to local machine
44scp -i {ssh_key_path} {user_name}@{public_ip}:REMOTE_FILE_PATH LOCAL_FILE_PATH
45scp -i {ssh_key_path} -r {user_name}@{public_ip}:REMOTE_DIRECTORY_PATH LOCAL_DIRECTORY_PATH
46
47# upload a local file to remote machine
48scp -i {ssh_key_path} LOCAL_FILE_PATH {user_name}@{public_ip}:REMOTE_FILE_PATH
49scp -i {ssh_key_path} -r LOCAL_DIRECTORY_PATH {user_name}@{public_ip}:REMOTE_DIRECTORY_PATH
50
51# AWS SSM session (requires a running SSM agent)
52# https://github.com/aws/amazon-ssm-agent/issues/131
53aws ssm start-session {profile_flag}--region {region} --target {instance_id}
54aws ssm start-session {profile_flag}--region {region} --target {instance_id} --document-name 'AWS-StartNonInteractiveCommand' --parameters command=\"sudo tail -10 /var/log/cloud-init-output.log\"
55aws ssm start-session {profile_flag}--region {region} --target {instance_id} --document-name 'AWS-StartInteractiveCommand' --parameters command=\"bash -l\"
56",
57            ssh_key_path = self.ssh_key_path,
58            user_name = self.user_name,
59
60            region = self.region,
61            availability_zone = self.availability_zone,
62
63            instance_id = self.instance_id,
64            instance_state = self.instance_state,
65
66            ip_mode = self.ip_mode,
67            public_ip = self.public_ip,
68
69            profile_flag = if let Some(v) = &self.profile {
70                format!("--profile {v} ")
71            } else {
72                String::new()
73            },
74        )
75    }
76}
77
78impl Command {
79    /// Run a command remotely.
80    pub fn run(&self, cmd: &str) -> io::Result<command_manager::Output> {
81        log::info!("sending an SSH command to {}", self.public_ip);
82        let remote_cmd_to_run = format!("chmod 400 {ssh_key_path} && ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} '{cmd}'",
83            ssh_key_path = self.ssh_key_path,
84            user_name = self.user_name,
85            public_ip = self.public_ip,
86        );
87        command_manager::run(&remote_cmd_to_run)
88    }
89
90    pub fn ssm_start_session_command(&self) -> String {
91        // aws ssm start-session --region [region] --target [instance ID]
92        format!(
93            "aws ssm start-session --region {region} --target {instance_id}",
94            region = self.region,
95            instance_id = self.instance_id,
96        )
97    }
98
99    /// Downloads a remote file to the local machine.
100    pub fn download_file(
101        &self,
102        remote_file_path: &str,
103        local_file_path: &str,
104        overwrite: bool,
105    ) -> io::Result<command_manager::Output> {
106        log::info!("sending an SCP command to {}", self.public_ip);
107        if Path::new(local_file_path).exists() && !overwrite {
108            return Err(io::Error::new(
109                io::ErrorKind::Other,
110                format!("file '{local_file_path}' already exists"),
111            ));
112        }
113        if overwrite {
114            let local_rm_cmd = format!("rm -f {local_file_path} || true");
115            let rm_out = command_manager::run(&local_rm_cmd)?;
116            log::info!("successfully rm '{local_file_path}' (out {:?})", rm_out);
117        };
118
119        let remote_cmd_to_run = format!("chmod 400 {ssh_key_path} && scp -i {ssh_key_path} {user_name}@{public_ip}:{remote_file_path} {local_file_path}",
120            ssh_key_path = self.ssh_key_path,
121            user_name = self.user_name,
122            public_ip = self.public_ip,
123            remote_file_path = remote_file_path,
124            local_file_path = local_file_path,
125        );
126        let out = command_manager::run(&remote_cmd_to_run)?;
127
128        if Path::new(local_file_path).exists() {
129            log::info!("successfully downloaded to '{local_file_path}'")
130        } else {
131            return Err(io::Error::new(
132                io::ErrorKind::Other,
133                format!("file '{local_file_path}' does not exist"),
134            ));
135        }
136
137        Ok(out)
138    }
139
140    /// Sends a local file to the remote machine.
141    pub fn send_file(
142        &self,
143        local_file_path: &str,
144        remote_file_path: &str,
145        overwrite: bool,
146    ) -> io::Result<command_manager::Output> {
147        log::info!("send_file to {}", self.public_ip);
148        if !Path::new(local_file_path).exists() {
149            return Err(io::Error::new(
150                io::ErrorKind::Other,
151                format!("file '{local_file_path}' does not exist"),
152            ));
153        }
154
155        if overwrite {
156            let remote_rm_cmd = format!("chmod 400 {ssh_key_path} && ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'sudo rm -f {remote_file_path} || true'",
157                ssh_key_path = self.ssh_key_path,
158                user_name = self.user_name,
159                public_ip = self.public_ip,
160            );
161            let rm_out = command_manager::run(&remote_rm_cmd)?;
162            log::info!("successfully rm '{remote_file_path}' (out {:?})", rm_out);
163        };
164
165        let remote_cmd_to_run = format!("chmod 400 {ssh_key_path} && scp -i {ssh_key_path} {local_file_path} {user_name}@{public_ip}:{remote_file_path}",
166            ssh_key_path = self.ssh_key_path,
167            user_name = self.user_name,
168            public_ip = self.public_ip,
169            local_file_path = local_file_path,
170            remote_file_path = remote_file_path,
171        );
172        let out = command_manager::run(&remote_cmd_to_run)?;
173
174        let remote_ls_cmd = format!("chmod 400 {ssh_key_path} && ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'ls {remote_file_path}'",
175            ssh_key_path = self.ssh_key_path,
176            user_name = self.user_name,
177            public_ip = self.public_ip,
178        );
179        let ls_out = command_manager::run(&remote_ls_cmd)?;
180        log::info!(
181            "successfully sent to '{remote_file_path}' (out {:?})",
182            ls_out
183        );
184
185        Ok(out)
186    }
187
188    /// Downloads a remote directory to the local machine.
189    pub fn download_directory(
190        &self,
191        remote_directory_path: &str,
192        local_directory_path: &str,
193        overwrite: bool,
194    ) -> io::Result<command_manager::Output> {
195        log::info!("download_directory from {}", self.public_ip);
196        if Path::new(local_directory_path).exists() && !overwrite {
197            return Err(io::Error::new(
198                io::ErrorKind::Other,
199                format!("directory '{local_directory_path}' already exists"),
200            ));
201        }
202        if overwrite {
203            let local_rm_cmd = format!("rm -rf {local_directory_path} || true");
204            let rm_out = command_manager::run(&local_rm_cmd)?;
205            log::info!(
206                "successfully rm '{local_directory_path}' (out {:?})",
207                rm_out
208            );
209        };
210
211        let remote_cmd_to_run = format!("chmod 400 {ssh_key_path} && scp -i {ssh_key_path} -r {user_name}@{public_ip}:{remote_directory_path} {local_directory_path}",
212            ssh_key_path = self.ssh_key_path,
213            user_name = self.user_name,
214            public_ip = self.public_ip,
215            remote_directory_path = remote_directory_path,
216            local_directory_path = local_directory_path,
217        );
218        let out = command_manager::run(&remote_cmd_to_run)?;
219
220        if Path::new(local_directory_path).exists() {
221            log::info!("successfully downloaded to '{local_directory_path}'")
222        } else {
223            return Err(io::Error::new(
224                io::ErrorKind::Other,
225                format!("directory '{local_directory_path}' does not exist"),
226            ));
227        }
228
229        Ok(out)
230    }
231
232    /// Sends a local directory to the remote machine.
233    pub fn send_directory(
234        &self,
235        local_directory_path: &str,
236        remote_directory_path: &str,
237        overwrite: bool,
238    ) -> io::Result<command_manager::Output> {
239        log::info!("send_directory to {}", self.public_ip);
240        if !Path::new(local_directory_path).exists() {
241            return Err(io::Error::new(
242                io::ErrorKind::Other,
243                format!("file '{local_directory_path}' does not exist"),
244            ));
245        }
246
247        if overwrite {
248            let remote_rm_cmd = format!("chmod 400 {ssh_key_path} && ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'sudo rm -f {remote_directory_path} || true'",
249                ssh_key_path = self.ssh_key_path,
250                user_name = self.user_name,
251                public_ip = self.public_ip,
252            );
253            let rm_out = command_manager::run(&remote_rm_cmd)?;
254            log::info!(
255                "successfully rm '{remote_directory_path}' (out {:?})",
256                rm_out
257            );
258        };
259
260        let remote_cmd_to_run = format!("chmod 400 {ssh_key_path} && scp -i {ssh_key_path} -r {local_directory_path} {user_name}@{public_ip}:{remote_directory_path}",
261            ssh_key_path = self.ssh_key_path,
262            user_name = self.user_name,
263            public_ip = self.public_ip,
264            local_directory_path = local_directory_path,
265            remote_directory_path = remote_directory_path,
266        );
267        let out = command_manager::run(&remote_cmd_to_run)?;
268
269        let remote_ls_cmd = format!("chmod 400 {ssh_key_path} && ssh -o \"StrictHostKeyChecking no\" -i {ssh_key_path} {user_name}@{public_ip} 'ls {remote_directory_path}'",
270            ssh_key_path = self.ssh_key_path,
271            user_name = self.user_name,
272            public_ip = self.public_ip,
273        );
274        let ls_out = command_manager::run(&remote_ls_cmd)?;
275        log::info!(
276            "successfully sent to '{remote_directory_path}' (out {:?})",
277            ls_out
278        );
279
280        Ok(out)
281    }
282}
283
284/// A list of ssh commands.
285pub struct Commands(pub Vec<Command>);
286
287impl Commands {
288    pub fn sync(&self, file_path: &str) -> io::Result<()> {
289        log::info!("syncing ssh commands to '{file_path}'");
290        let path = Path::new(file_path);
291        let parent_dir = path.parent().unwrap();
292        fs::create_dir_all(parent_dir)?;
293
294        let mut contents = String::from("#!/bin/bash\n\n");
295        for ssh_cmd in self.0.iter() {
296            let d = ssh_cmd.to_string();
297            contents.push_str(&d);
298            contents.push_str("\n\n");
299        }
300
301        let mut f = File::create(file_path)?;
302        f.write_all(&contents.as_bytes())?;
303
304        Ok(())
305    }
306}