1use crate::config::PostgresConfig;
11use crate::error::{Error, Result};
12use lmrc_ssh::SshClient;
13use tracing::{debug, info, warn};
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum Platform {
18 Debian,
20 Ubuntu,
22 RedHat,
24 CentOS,
26 Alpine,
28 Unknown(String),
30}
31
32impl Platform {
33 pub fn as_str(&self) -> &str {
35 match self {
36 Platform::Debian => "Debian",
37 Platform::Ubuntu => "Ubuntu",
38 Platform::RedHat => "RedHat",
39 Platform::CentOS => "CentOS",
40 Platform::Alpine => "Alpine",
41 Platform::Unknown(s) => s,
42 }
43 }
44
45 pub fn is_supported(&self) -> bool {
47 matches!(self, Platform::Debian | Platform::Ubuntu)
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct SystemRequirements {
54 pub min_ram_mb: u64,
56 pub min_disk_mb: u64,
58 pub min_cpu_cores: u32,
60}
61
62impl Default for SystemRequirements {
63 fn default() -> Self {
64 Self {
65 min_ram_mb: 1024, min_disk_mb: 5120, min_cpu_cores: 1,
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct SystemInfo {
75 pub platform: Platform,
77 pub os_version: String,
79 pub total_ram_mb: u64,
81 pub free_disk_mb: u64,
83 pub cpu_cores: u32,
85}
86
87pub type ProgressCallback = Box<dyn Fn(InstallationStep, u8) + Send + Sync>;
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum InstallationStep {
93 CheckingRequirements,
95 DetectingPlatform,
97 CheckingVersionAvailability,
99 InstallingPrerequisites,
101 AddingRepository,
103 UpdatingPackages,
105 InstallingPostgres,
107 StartingService,
109 VerifyingInstallation,
111 Complete,
113}
114
115impl InstallationStep {
116 pub fn description(&self) -> &str {
118 match self {
119 Self::CheckingRequirements => "Checking system requirements",
120 Self::DetectingPlatform => "Detecting platform",
121 Self::CheckingVersionAvailability => "Checking PostgreSQL version availability",
122 Self::InstallingPrerequisites => "Installing prerequisites",
123 Self::AddingRepository => "Adding PostgreSQL repository",
124 Self::UpdatingPackages => "Updating package list",
125 Self::InstallingPostgres => "Installing PostgreSQL",
126 Self::StartingService => "Starting PostgreSQL service",
127 Self::VerifyingInstallation => "Verifying installation",
128 Self::Complete => "Installation complete",
129 }
130 }
131}
132
133pub async fn detect_platform(ssh: &mut SshClient) -> Result<Platform> {
135 debug!("Detecting platform");
136
137 let result = ssh.execute("cat /etc/os-release 2>/dev/null || cat /etc/lsb-release 2>/dev/null");
139
140 if let Ok(output) = result {
141 let content = output.stdout.to_lowercase();
142
143 if content.contains("ubuntu") {
144 return Ok(Platform::Ubuntu);
145 } else if content.contains("debian") {
146 return Ok(Platform::Debian);
147 } else if content.contains("rhel") || content.contains("red hat") {
148 return Ok(Platform::RedHat);
149 } else if content.contains("centos") {
150 return Ok(Platform::CentOS);
151 } else if content.contains("alpine") {
152 return Ok(Platform::Alpine);
153 }
154 }
155
156 if ssh.execute("test -f /etc/debian_version").is_ok() {
158 return Ok(Platform::Debian);
159 }
160
161 warn!("Could not detect platform, using Unknown");
162 Ok(Platform::Unknown("Unknown".to_string()))
163}
164
165pub async fn get_system_info(ssh: &mut SshClient) -> Result<SystemInfo> {
167 debug!("Gathering system information");
168
169 let platform = detect_platform(ssh).await?;
170
171 let os_version = ssh
173 .execute("lsb_release -d 2>/dev/null | cut -f2 || cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"'")
174 .map(|o| o.stdout.trim().to_string())
175 .unwrap_or_else(|_| "Unknown".to_string());
176
177 let total_ram_mb = ssh
179 .execute("free -m | grep Mem: | awk '{print $2}'")
180 .ok()
181 .and_then(|o| o.stdout.trim().parse().ok())
182 .unwrap_or(0);
183
184 let free_disk_mb = ssh
186 .execute("df -m / | tail -1 | awk '{print $4}'")
187 .ok()
188 .and_then(|o| o.stdout.trim().parse().ok())
189 .unwrap_or(0);
190
191 let cpu_cores = ssh
193 .execute("nproc")
194 .ok()
195 .and_then(|o| o.stdout.trim().parse().ok())
196 .unwrap_or(1);
197
198 Ok(SystemInfo {
199 platform,
200 os_version,
201 total_ram_mb,
202 free_disk_mb,
203 cpu_cores,
204 })
205}
206
207pub async fn check_requirements(
209 ssh: &mut SshClient,
210 requirements: &SystemRequirements,
211) -> Result<SystemInfo> {
212 info!("Checking system requirements");
213
214 let sys_info = get_system_info(ssh).await?;
215
216 debug!("System info: {:?}", sys_info);
217
218 if !sys_info.platform.is_supported() {
220 return Err(Error::Installation(format!(
221 "Unsupported platform: {}. Currently only Debian and Ubuntu are supported.",
222 sys_info.platform.as_str()
223 )));
224 }
225
226 if sys_info.total_ram_mb < requirements.min_ram_mb {
228 return Err(Error::Installation(format!(
229 "Insufficient RAM: {}MB available, {}MB required",
230 sys_info.total_ram_mb, requirements.min_ram_mb
231 )));
232 }
233
234 if sys_info.free_disk_mb < requirements.min_disk_mb {
236 return Err(Error::Installation(format!(
237 "Insufficient disk space: {}MB available, {}MB required",
238 sys_info.free_disk_mb, requirements.min_disk_mb
239 )));
240 }
241
242 if sys_info.cpu_cores < requirements.min_cpu_cores {
244 warn!(
245 "Low CPU cores: {} available, {} recommended",
246 sys_info.cpu_cores, requirements.min_cpu_cores
247 );
248 }
249
250 info!("✓ System requirements check passed");
251 Ok(sys_info)
252}
253
254pub async fn check_version_available(ssh: &mut SshClient, version: &str) -> Result<bool> {
256 debug!("Checking if PostgreSQL {} is available", version);
257
258 let has_repo = ssh
260 .execute("test -f /etc/apt/sources.list.d/pgdg.list")
261 .is_ok();
262
263 if !has_repo {
264 ssh.execute("DEBIAN_FRONTEND=noninteractive apt-get update -y")
266 .ok();
267 ssh.execute("DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg2 wget lsb-release")
268 .ok();
269 ssh.execute(
270 "wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -",
271 )
272 .ok();
273 ssh.execute(r#"echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list"#).ok();
274 ssh.execute("apt-get update -y").ok();
275 }
276
277 let package_name = format!("postgresql-{}", version);
279 let result = ssh.execute(&format!("apt-cache show {} 2>&1", package_name));
280
281 match result {
282 Ok(output) => {
283 let available = output
284 .stdout
285 .contains(&format!("Package: {}", package_name));
286 debug!("PostgreSQL {} available: {}", version, available);
287 Ok(available)
288 }
289 Err(_) => {
290 debug!("PostgreSQL {} not found in repositories", version);
291 Ok(false)
292 }
293 }
294}
295
296pub async fn upgrade(
298 ssh: &mut SshClient,
299 from_version: &str,
300 to_version: &str,
301 _config: &PostgresConfig,
302) -> Result<()> {
303 info!(
304 "Upgrading PostgreSQL from {} to {}",
305 from_version, to_version
306 );
307
308 if !check_version_available(ssh, to_version).await? {
310 return Err(Error::Installation(format!(
311 "PostgreSQL version {} is not available",
312 to_version
313 )));
314 }
315
316 info!("Stopping PostgreSQL {}", from_version);
318 ssh.execute("systemctl stop postgresql")
319 .map_err(|e| Error::ServiceError(format!("Failed to stop service: {}", e)))?;
320
321 info!("Installing PostgreSQL {}", to_version);
323 let install_cmd = format!(
324 "DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-{}",
325 to_version
326 );
327 ssh.execute(&install_cmd)
328 .map_err(|e| Error::Installation(format!("Failed to install new version: {}", e)))?;
329
330 info!("Note: Manual data migration may be required");
332 warn!(
333 "PostgreSQL {} installed. You may need to migrate data from version {}",
334 to_version, from_version
335 );
336
337 info!("Starting PostgreSQL {}", to_version);
339 ssh.execute("systemctl start postgresql")
340 .map_err(|e| Error::ServiceError(format!("Failed to start service: {}", e)))?;
341
342 info!("Upgrade to PostgreSQL {} completed", to_version);
343 info!("⚠️ Please verify your data and configuration");
344
345 Ok(())
346}
347
348pub async fn verify_installation(ssh: &mut SshClient, config: &PostgresConfig) -> Result<()> {
350 info!("Performing comprehensive installation verification");
351
352 let psql_check = ssh.execute("which psql");
354 if psql_check.is_err() {
355 return Err(Error::Installation(
356 "PostgreSQL not found in PATH".to_string(),
357 ));
358 }
359
360 let version_output = ssh
362 .execute("psql --version")
363 .map_err(|e| Error::Installation(format!("Failed to get version: {}", e)))?;
364
365 if !version_output.stdout.contains(&config.version) {
366 return Err(Error::Installation(format!(
367 "Version mismatch: expected {}, got {}",
368 config.version, version_output.stdout
369 )));
370 }
371 info!("✓ PostgreSQL version {} confirmed", config.version);
372
373 let service_status = ssh
375 .execute("systemctl is-active postgresql")
376 .map_err(|e| Error::ServiceError(format!("Failed to check service: {}", e)))?;
377
378 if service_status.stdout.trim() != "active" {
379 return Err(Error::ServiceError("Service is not active".to_string()));
380 }
381 info!("✓ PostgreSQL service is running");
382
383 let service_enabled = ssh
385 .execute("systemctl is-enabled postgresql")
386 .map_err(|e| Error::ServiceError(format!("Failed to check if enabled: {}", e)))?;
387
388 if service_enabled.stdout.trim() != "enabled" {
389 warn!("PostgreSQL service is not enabled for auto-start");
390 } else {
391 info!("✓ PostgreSQL service is enabled");
392 }
393
394 let listening = ssh
396 .execute("ss -tlnp | grep postgres || netstat -tlnp | grep postgres")
397 .is_ok();
398
399 if !listening {
400 warn!("PostgreSQL may not be listening on expected port");
401 } else {
402 info!("✓ PostgreSQL is listening for connections");
403 }
404
405 let config_dir = config.config_dir();
407 let conf_exists = ssh
408 .execute(&format!("test -f {}/postgresql.conf", config_dir))
409 .is_ok();
410
411 if !conf_exists {
412 return Err(Error::Configuration(format!(
413 "Configuration file not found: {}/postgresql.conf",
414 config_dir
415 )));
416 }
417 info!("✓ Configuration files exist");
418
419 let data_dir_check = ssh
421 .execute(&format!("test -d /var/lib/postgresql/{}", config.version))
422 .is_ok();
423
424 if !data_dir_check {
425 warn!("Data directory may not exist");
426 } else {
427 info!("✓ Data directory exists");
428 }
429
430 info!("✅ Installation verification complete");
431 Ok(())
432}
433
434pub async fn rollback_installation(ssh: &mut SshClient, version: &str) -> Result<()> {
436 warn!("Rolling back PostgreSQL {} installation", version);
437
438 let _ = ssh.execute("systemctl stop postgresql");
440
441 let remove_cmd = format!("apt-get remove -y postgresql-{}", version);
443 ssh.execute(&remove_cmd)
444 .map_err(|e| Error::Uninstallation(format!("Rollback failed: {}", e)))?;
445
446 let _ = ssh.execute("apt-get autoremove -y");
448
449 info!("Rollback completed");
450 Ok(())
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_platform_detection() {
459 let platform = Platform::Ubuntu;
460 assert_eq!(platform.as_str(), "Ubuntu");
461 assert!(platform.is_supported());
462
463 let unknown = Platform::Unknown("Custom".to_string());
464 assert_eq!(unknown.as_str(), "Custom");
465 assert!(!unknown.is_supported());
466 }
467
468 #[test]
469 fn test_system_requirements_default() {
470 let req = SystemRequirements::default();
471 assert_eq!(req.min_ram_mb, 1024);
472 assert_eq!(req.min_disk_mb, 5120);
473 assert_eq!(req.min_cpu_cores, 1);
474 }
475
476 #[test]
477 fn test_installation_step_description() {
478 let step = InstallationStep::CheckingRequirements;
479 assert_eq!(step.description(), "Checking system requirements");
480
481 let step = InstallationStep::InstallingPostgres;
482 assert_eq!(step.description(), "Installing PostgreSQL");
483 }
484}