use crate::config::PostgresConfig;
use crate::error::{Error, Result};
use lmrc_ssh::SshClient;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, PartialEq)]
pub enum Platform {
Debian,
Ubuntu,
RedHat,
CentOS,
Alpine,
Unknown(String),
}
impl Platform {
pub fn as_str(&self) -> &str {
match self {
Platform::Debian => "Debian",
Platform::Ubuntu => "Ubuntu",
Platform::RedHat => "RedHat",
Platform::CentOS => "CentOS",
Platform::Alpine => "Alpine",
Platform::Unknown(s) => s,
}
}
pub fn is_supported(&self) -> bool {
matches!(self, Platform::Debian | Platform::Ubuntu)
}
}
#[derive(Debug, Clone)]
pub struct SystemRequirements {
pub min_ram_mb: u64,
pub min_disk_mb: u64,
pub min_cpu_cores: u32,
}
impl Default for SystemRequirements {
fn default() -> Self {
Self {
min_ram_mb: 1024, min_disk_mb: 5120, min_cpu_cores: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct SystemInfo {
pub platform: Platform,
pub os_version: String,
pub total_ram_mb: u64,
pub free_disk_mb: u64,
pub cpu_cores: u32,
}
pub type ProgressCallback = Box<dyn Fn(InstallationStep, u8) + Send + Sync>;
#[derive(Debug, Clone, PartialEq)]
pub enum InstallationStep {
CheckingRequirements,
DetectingPlatform,
CheckingVersionAvailability,
InstallingPrerequisites,
AddingRepository,
UpdatingPackages,
InstallingPostgres,
StartingService,
VerifyingInstallation,
Complete,
}
impl InstallationStep {
pub fn description(&self) -> &str {
match self {
Self::CheckingRequirements => "Checking system requirements",
Self::DetectingPlatform => "Detecting platform",
Self::CheckingVersionAvailability => "Checking PostgreSQL version availability",
Self::InstallingPrerequisites => "Installing prerequisites",
Self::AddingRepository => "Adding PostgreSQL repository",
Self::UpdatingPackages => "Updating package list",
Self::InstallingPostgres => "Installing PostgreSQL",
Self::StartingService => "Starting PostgreSQL service",
Self::VerifyingInstallation => "Verifying installation",
Self::Complete => "Installation complete",
}
}
}
pub async fn detect_platform(ssh: &mut SshClient) -> Result<Platform> {
debug!("Detecting platform");
let result = ssh.execute("cat /etc/os-release 2>/dev/null || cat /etc/lsb-release 2>/dev/null");
if let Ok(output) = result {
let content = output.stdout.to_lowercase();
if content.contains("ubuntu") {
return Ok(Platform::Ubuntu);
} else if content.contains("debian") {
return Ok(Platform::Debian);
} else if content.contains("rhel") || content.contains("red hat") {
return Ok(Platform::RedHat);
} else if content.contains("centos") {
return Ok(Platform::CentOS);
} else if content.contains("alpine") {
return Ok(Platform::Alpine);
}
}
if ssh.execute("test -f /etc/debian_version").is_ok() {
return Ok(Platform::Debian);
}
warn!("Could not detect platform, using Unknown");
Ok(Platform::Unknown("Unknown".to_string()))
}
pub async fn get_system_info(ssh: &mut SshClient) -> Result<SystemInfo> {
debug!("Gathering system information");
let platform = detect_platform(ssh).await?;
let os_version = ssh
.execute("lsb_release -d 2>/dev/null | cut -f2 || cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"'")
.map(|o| o.stdout.trim().to_string())
.unwrap_or_else(|_| "Unknown".to_string());
let total_ram_mb = ssh
.execute("free -m | grep Mem: | awk '{print $2}'")
.ok()
.and_then(|o| o.stdout.trim().parse().ok())
.unwrap_or(0);
let free_disk_mb = ssh
.execute("df -m / | tail -1 | awk '{print $4}'")
.ok()
.and_then(|o| o.stdout.trim().parse().ok())
.unwrap_or(0);
let cpu_cores = ssh
.execute("nproc")
.ok()
.and_then(|o| o.stdout.trim().parse().ok())
.unwrap_or(1);
Ok(SystemInfo {
platform,
os_version,
total_ram_mb,
free_disk_mb,
cpu_cores,
})
}
pub async fn check_requirements(
ssh: &mut SshClient,
requirements: &SystemRequirements,
) -> Result<SystemInfo> {
info!("Checking system requirements");
let sys_info = get_system_info(ssh).await?;
debug!("System info: {:?}", sys_info);
if !sys_info.platform.is_supported() {
return Err(Error::Installation(format!(
"Unsupported platform: {}. Currently only Debian and Ubuntu are supported.",
sys_info.platform.as_str()
)));
}
if sys_info.total_ram_mb < requirements.min_ram_mb {
return Err(Error::Installation(format!(
"Insufficient RAM: {}MB available, {}MB required",
sys_info.total_ram_mb, requirements.min_ram_mb
)));
}
if sys_info.free_disk_mb < requirements.min_disk_mb {
return Err(Error::Installation(format!(
"Insufficient disk space: {}MB available, {}MB required",
sys_info.free_disk_mb, requirements.min_disk_mb
)));
}
if sys_info.cpu_cores < requirements.min_cpu_cores {
warn!(
"Low CPU cores: {} available, {} recommended",
sys_info.cpu_cores, requirements.min_cpu_cores
);
}
info!("✓ System requirements check passed");
Ok(sys_info)
}
pub async fn check_version_available(ssh: &mut SshClient, version: &str) -> Result<bool> {
debug!("Checking if PostgreSQL {} is available", version);
let has_repo = ssh
.execute("test -f /etc/apt/sources.list.d/pgdg.list")
.is_ok();
if !has_repo {
ssh.execute("DEBIAN_FRONTEND=noninteractive apt-get update -y")
.ok();
ssh.execute("DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg2 wget lsb-release")
.ok();
ssh.execute(
"wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -",
)
.ok();
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();
ssh.execute("apt-get update -y").ok();
}
let package_name = format!("postgresql-{}", version);
let result = ssh.execute(&format!("apt-cache show {} 2>&1", package_name));
match result {
Ok(output) => {
let available = output
.stdout
.contains(&format!("Package: {}", package_name));
debug!("PostgreSQL {} available: {}", version, available);
Ok(available)
}
Err(_) => {
debug!("PostgreSQL {} not found in repositories", version);
Ok(false)
}
}
}
pub async fn upgrade(
ssh: &mut SshClient,
from_version: &str,
to_version: &str,
_config: &PostgresConfig,
) -> Result<()> {
info!(
"Upgrading PostgreSQL from {} to {}",
from_version, to_version
);
if !check_version_available(ssh, to_version).await? {
return Err(Error::Installation(format!(
"PostgreSQL version {} is not available",
to_version
)));
}
info!("Stopping PostgreSQL {}", from_version);
ssh.execute("systemctl stop postgresql")
.map_err(|e| Error::ServiceError(format!("Failed to stop service: {}", e)))?;
info!("Installing PostgreSQL {}", to_version);
let install_cmd = format!(
"DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-{}",
to_version
);
ssh.execute(&install_cmd)
.map_err(|e| Error::Installation(format!("Failed to install new version: {}", e)))?;
info!("Note: Manual data migration may be required");
warn!(
"PostgreSQL {} installed. You may need to migrate data from version {}",
to_version, from_version
);
info!("Starting PostgreSQL {}", to_version);
ssh.execute("systemctl start postgresql")
.map_err(|e| Error::ServiceError(format!("Failed to start service: {}", e)))?;
info!("Upgrade to PostgreSQL {} completed", to_version);
info!("⚠️ Please verify your data and configuration");
Ok(())
}
pub async fn verify_installation(ssh: &mut SshClient, config: &PostgresConfig) -> Result<()> {
info!("Performing comprehensive installation verification");
let psql_check = ssh.execute("which psql");
if psql_check.is_err() {
return Err(Error::Installation(
"PostgreSQL not found in PATH".to_string(),
));
}
let version_output = ssh
.execute("psql --version")
.map_err(|e| Error::Installation(format!("Failed to get version: {}", e)))?;
if !version_output.stdout.contains(&config.version) {
return Err(Error::Installation(format!(
"Version mismatch: expected {}, got {}",
config.version, version_output.stdout
)));
}
info!("✓ PostgreSQL version {} confirmed", config.version);
let service_status = ssh
.execute("systemctl is-active postgresql")
.map_err(|e| Error::ServiceError(format!("Failed to check service: {}", e)))?;
if service_status.stdout.trim() != "active" {
return Err(Error::ServiceError("Service is not active".to_string()));
}
info!("✓ PostgreSQL service is running");
let service_enabled = ssh
.execute("systemctl is-enabled postgresql")
.map_err(|e| Error::ServiceError(format!("Failed to check if enabled: {}", e)))?;
if service_enabled.stdout.trim() != "enabled" {
warn!("PostgreSQL service is not enabled for auto-start");
} else {
info!("✓ PostgreSQL service is enabled");
}
let listening = ssh
.execute("ss -tlnp | grep postgres || netstat -tlnp | grep postgres")
.is_ok();
if !listening {
warn!("PostgreSQL may not be listening on expected port");
} else {
info!("✓ PostgreSQL is listening for connections");
}
let config_dir = config.config_dir();
let conf_exists = ssh
.execute(&format!("test -f {}/postgresql.conf", config_dir))
.is_ok();
if !conf_exists {
return Err(Error::Configuration(format!(
"Configuration file not found: {}/postgresql.conf",
config_dir
)));
}
info!("✓ Configuration files exist");
let data_dir_check = ssh
.execute(&format!("test -d /var/lib/postgresql/{}", config.version))
.is_ok();
if !data_dir_check {
warn!("Data directory may not exist");
} else {
info!("✓ Data directory exists");
}
info!("✅ Installation verification complete");
Ok(())
}
pub async fn rollback_installation(ssh: &mut SshClient, version: &str) -> Result<()> {
warn!("Rolling back PostgreSQL {} installation", version);
let _ = ssh.execute("systemctl stop postgresql");
let remove_cmd = format!("apt-get remove -y postgresql-{}", version);
ssh.execute(&remove_cmd)
.map_err(|e| Error::Uninstallation(format!("Rollback failed: {}", e)))?;
let _ = ssh.execute("apt-get autoremove -y");
info!("Rollback completed");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let platform = Platform::Ubuntu;
assert_eq!(platform.as_str(), "Ubuntu");
assert!(platform.is_supported());
let unknown = Platform::Unknown("Custom".to_string());
assert_eq!(unknown.as_str(), "Custom");
assert!(!unknown.is_supported());
}
#[test]
fn test_system_requirements_default() {
let req = SystemRequirements::default();
assert_eq!(req.min_ram_mb, 1024);
assert_eq!(req.min_disk_mb, 5120);
assert_eq!(req.min_cpu_cores, 1);
}
#[test]
fn test_installation_step_description() {
let step = InstallationStep::CheckingRequirements;
assert_eq!(step.description(), "Checking system requirements");
let step = InstallationStep::InstallingPostgres;
assert_eq!(step.description(), "Installing PostgreSQL");
}
}