mecha10_deploy/services/deployment/
mod.rs1#![allow(dead_code)]
2
3mod types;
9
10pub use types::{DeployConfig, DeployResult, DeployStrategy};
11
12use anyhow::{Context, Result};
13use std::path::Path;
14use std::process::Command;
15
16pub struct DeploymentService;
17
18impl DeploymentService {
19 pub fn new() -> Self {
21 Self
22 }
23
24 pub fn test_connection(&self, config: &DeployConfig) -> Result<()> {
30 let mut cmd = Command::new("ssh");
31
32 cmd.arg("-p").arg(config.port.to_string());
33
34 if let Some(key_path) = &config.ssh_key {
35 cmd.arg("-i").arg(key_path);
36 }
37
38 cmd.arg("-o")
39 .arg("ConnectTimeout=5")
40 .arg("-o")
41 .arg("StrictHostKeyChecking=no")
42 .arg(format!("{}@{}", config.user, config.host))
43 .arg("echo 'Connection test successful'");
44
45 let output = cmd.output().context("Failed to execute SSH command")?;
46
47 if !output.status.success() {
48 let stderr = String::from_utf8_lossy(&output.stderr);
49 return Err(anyhow::anyhow!(
50 "SSH connection failed: {}. Please check:\n\
51 - Host is reachable: {}\n\
52 - Port is correct: {}\n\
53 - SSH key is valid: {:?}\n\
54 - User has access: {}",
55 stderr,
56 config.host,
57 config.port,
58 config.ssh_key,
59 config.user
60 ));
61 }
62
63 Ok(())
64 }
65
66 pub fn create_backup(&self, config: &DeployConfig) -> Result<()> {
72 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
73 let backup_name = format!("backup_{}", timestamp);
74
75 let remote_cmd = format!(
76 "mkdir -p {} && [ -d {} ] && cp -r {} {}/{} || echo 'No previous installation to backup'",
77 config.backup_dir, config.remote_dir, config.remote_dir, config.backup_dir, backup_name
78 );
79
80 self.run_remote_command(config, &remote_cmd)?;
81
82 Ok(())
83 }
84
85 pub fn upload_package(&self, config: &DeployConfig, package_path: &Path) -> Result<()> {
92 if !package_path.exists() {
93 return Err(anyhow::anyhow!("Package not found: {}", package_path.display()));
94 }
95
96 let remote_cmd = format!("mkdir -p {}/staging", config.remote_dir);
97 self.run_remote_command(config, &remote_cmd)?;
98
99 let mut cmd = Command::new("rsync");
100
101 cmd.arg("-avz")
102 .arg("--progress")
103 .arg("-e")
104 .arg(format!("ssh -p {}", config.port));
105
106 if let Some(key_path) = &config.ssh_key {
107 cmd.arg("-e")
108 .arg(format!("ssh -p {} -i {}", config.port, key_path.display()));
109 }
110
111 cmd.arg(package_path).arg(format!(
112 "{}@{}:{}/staging/",
113 config.user, config.host, config.remote_dir
114 ));
115
116 let status = cmd.status().context("Failed to execute rsync")?;
117
118 if !status.success() {
119 return Err(anyhow::anyhow!("Package upload failed"));
120 }
121
122 let package_name = package_path.file_name().unwrap().to_string_lossy().to_string();
123 let extract_cmd = format!("cd {}/staging && tar -xzf {}", config.remote_dir, package_name);
124 self.run_remote_command(config, &extract_cmd)?;
125
126 Ok(())
127 }
128
129 pub async fn deploy(
137 &self,
138 config: &DeployConfig,
139 package_path: &Path,
140 strategy: DeployStrategy,
141 ) -> Result<DeployResult> {
142 let backup_created = self.create_backup(config).is_ok();
143
144 self.upload_package(config, package_path)?;
145
146 match strategy {
147 DeployStrategy::Direct => self.deploy_direct(config).await?,
148 DeployStrategy::Rolling => self.deploy_rolling(config).await?,
149 DeployStrategy::BlueGreen => self.deploy_blue_green(config).await?,
150 }
151
152 let health_check_passed = self.health_check(config).await.unwrap_or(false);
153
154 if !health_check_passed {
155 self.rollback(config).await?;
156
157 return Ok(DeployResult {
158 success: false,
159 message: "Deployment failed health check, rolled back".to_string(),
160 health_check_passed: false,
161 backup_created,
162 });
163 }
164
165 Ok(DeployResult {
166 success: true,
167 message: format!("Deployment successful using {} strategy", strategy.as_str()),
168 health_check_passed: true,
169 backup_created,
170 })
171 }
172
173 async fn deploy_direct(&self, config: &DeployConfig) -> Result<()> {
175 let stop_cmd = format!("sudo systemctl stop {}", config.service_name);
176 self.run_remote_command(config, &stop_cmd)?;
177
178 let deploy_cmd = format!("cp -r {}/staging/* {}/", config.remote_dir, config.remote_dir);
179 self.run_remote_command(config, &deploy_cmd)?;
180
181 let start_cmd = format!("sudo systemctl start {}", config.service_name);
182 self.run_remote_command(config, &start_cmd)?;
183
184 Ok(())
185 }
186
187 async fn deploy_rolling(&self, config: &DeployConfig) -> Result<()> {
189 self.deploy_direct(config).await
190 }
191
192 async fn deploy_blue_green(&self, config: &DeployConfig) -> Result<()> {
194 self.deploy_direct(config).await
195 }
196
197 pub async fn health_check(&self, config: &DeployConfig) -> Result<bool> {
203 for attempt in 1..=config.health_check_retries {
204 let check_cmd = format!("systemctl is-active {}", config.service_name);
205
206 match self.run_remote_command(config, &check_cmd) {
207 Ok(output) if output.trim() == "active" => return Ok(true),
208 _ => {
209 if attempt < config.health_check_retries {
210 tokio::time::sleep(tokio::time::Duration::from_secs(
211 config.health_check_timeout / config.health_check_retries as u64,
212 ))
213 .await;
214 }
215 }
216 }
217 }
218
219 Ok(false)
220 }
221
222 pub async fn rollback(&self, config: &DeployConfig) -> Result<()> {
228 let find_backup_cmd = format!("ls -t {}/backup_* | head -1", config.backup_dir);
229 let latest_backup = self.run_remote_command(config, &find_backup_cmd)?.trim().to_string();
230
231 if latest_backup.is_empty() {
232 return Err(anyhow::anyhow!("No backup found for rollback"));
233 }
234
235 let stop_cmd = format!("sudo systemctl stop {}", config.service_name);
236 self.run_remote_command(config, &stop_cmd)?;
237
238 let restore_cmd = format!(
239 "rm -rf {} && cp -r {} {}",
240 config.remote_dir, latest_backup, config.remote_dir
241 );
242 self.run_remote_command(config, &restore_cmd)?;
243
244 let start_cmd = format!("sudo systemctl start {}", config.service_name);
245 self.run_remote_command(config, &start_cmd)?;
246
247 Ok(())
248 }
249
250 pub fn run_remote_command(&self, config: &DeployConfig, command: &str) -> Result<String> {
257 let mut cmd = Command::new("ssh");
258
259 cmd.arg("-p").arg(config.port.to_string());
260
261 if let Some(key_path) = &config.ssh_key {
262 cmd.arg("-i").arg(key_path);
263 }
264
265 cmd.arg("-o")
266 .arg("StrictHostKeyChecking=no")
267 .arg(format!("{}@{}", config.user, config.host))
268 .arg(command);
269
270 let output = cmd.output().context("Failed to execute SSH command")?;
271
272 if !output.status.success() {
273 let stderr = String::from_utf8_lossy(&output.stderr);
274 return Err(anyhow::anyhow!("Remote command failed: {}", stderr));
275 }
276
277 Ok(String::from_utf8_lossy(&output.stdout).to_string())
278 }
279
280 pub fn get_service_status(&self, config: &DeployConfig) -> Result<String> {
286 let cmd = format!("systemctl status {}", config.service_name);
287 self.run_remote_command(config, &cmd)
288 }
289
290 pub fn list_backups(&self, config: &DeployConfig) -> Result<Vec<String>> {
296 let cmd = format!("ls -t {}/backup_* 2>/dev/null || echo ''", config.backup_dir);
297 let output = self.run_remote_command(config, &cmd)?;
298
299 let backups: Vec<String> = output
300 .lines()
301 .filter(|line| !line.is_empty())
302 .map(|line| line.to_string())
303 .collect();
304
305 Ok(backups)
306 }
307}
308
309impl Default for DeploymentService {
310 fn default() -> Self {
311 Self::new()
312 }
313}