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
26impl std::fmt::Display for Command {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 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 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 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 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 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 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 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
284pub 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}