use crate::config::PostgresConfig;
use crate::diff::ConfigDiff;
use crate::error::{Error, Result};
use crate::operations;
use lmrc_ssh::{AuthMethod, SshClient};
use tracing::{info, warn};
pub struct PostgresManager {
config: PostgresConfig,
server_ip: String,
ssh_user: String,
ssh_password: Option<String>,
ssh_key_path: Option<String>,
ssh_port: u16,
private_ip: Option<String>,
}
impl PostgresManager {
pub fn builder() -> PostgresManagerBuilder {
PostgresManagerBuilder::default()
}
fn connect(&self) -> Result<SshClient> {
let auth = if let Some(ref password) = self.ssh_password {
AuthMethod::Password {
username: self.ssh_user.clone(),
password: password.clone(),
}
} else if let Some(ref key_path) = self.ssh_key_path {
AuthMethod::PublicKey {
username: self.ssh_user.clone(),
private_key_path: key_path.clone(),
passphrase: None,
}
} else {
let ssh_key_path = std::env::var("SSH_KEY_PATH")
.unwrap_or_else(|_| ".ssh/id_rsa".to_string());
AuthMethod::PublicKey {
username: self.ssh_user.clone(),
private_key_path: ssh_key_path,
passphrase: None,
}
};
SshClient::new(&self.server_ip, self.ssh_port)?
.with_auth(auth)
.connect()
.map_err(Error::Ssh)
}
pub async fn is_installed(&self) -> Result<bool> {
let mut ssh = self.connect()?;
operations::is_installed(&mut ssh).await
}
pub async fn get_installed_version(&self) -> Result<Option<String>> {
let mut ssh = self.connect()?;
operations::get_installed_version(&mut ssh).await
}
pub async fn install(&self) -> Result<()> {
let mut ssh = self.connect()?;
operations::install(&mut ssh, &self.config).await
}
pub async fn uninstall(&self, purge: bool) -> Result<()> {
let mut ssh = self.connect()?;
operations::uninstall(&mut ssh, &self.config, purge).await
}
pub async fn configure_database(&self) -> Result<()> {
let mut ssh = self.connect()?;
operations::configure_database(&mut ssh, &self.config).await
}
pub async fn configure_server(&self) -> Result<()> {
let mut ssh = self.connect()?;
operations::configure_server(&mut ssh, &self.config).await
}
pub async fn configure(&self) -> Result<()> {
self.configure_database().await?;
self.configure_server().await?;
Ok(())
}
pub async fn setup(&self) -> Result<()> {
if !self.is_installed().await? {
self.install().await?;
} else {
info!("PostgreSQL is already installed");
}
self.configure().await?;
if let Some(ref priv_ip) = self.private_ip {
info!(
"Database is accessible at: postgresql://{}:***@{}:{}/{}",
self.config.username, priv_ip, self.config.port, self.config.database_name
);
}
Ok(())
}
pub async fn diff(&self) -> Result<ConfigDiff> {
let mut ssh = self.connect()?;
operations::detect_diff(&mut ssh, &self.config).await
}
pub async fn apply_diff(&self, diff: &ConfigDiff) -> Result<()> {
if !diff.has_changes() {
info!("No changes to apply");
return Ok(());
}
info!("Applying configuration changes: {}", diff.summary());
self.configure_server().await?;
info!("Configuration changes applied successfully");
Ok(())
}
pub async fn test_connection(&self) -> Result<()> {
let mut ssh = self.connect()?;
operations::test_connection(&mut ssh, &self.config).await
}
pub fn config(&self) -> &PostgresConfig {
&self.config
}
pub fn server_ip(&self) -> &str {
&self.server_ip
}
pub fn ssh_user(&self) -> &str {
&self.ssh_user
}
pub fn private_ip(&self) -> Option<&str> {
self.private_ip.as_deref()
}
pub async fn detect_platform(&self) -> Result<crate::install::Platform> {
let mut ssh = self.connect()?;
crate::install::detect_platform(&mut ssh).await
}
pub async fn get_system_info(&self) -> Result<crate::install::SystemInfo> {
let mut ssh = self.connect()?;
crate::install::get_system_info(&mut ssh).await
}
pub async fn check_requirements(
&self,
requirements: Option<crate::install::SystemRequirements>,
) -> Result<crate::install::SystemInfo> {
let mut ssh = self.connect()?;
let req = requirements.unwrap_or_default();
crate::install::check_requirements(&mut ssh, &req).await
}
pub async fn check_version_available(&self, version: &str) -> Result<bool> {
let mut ssh = self.connect()?;
crate::install::check_version_available(&mut ssh, version).await
}
pub async fn verify_installation(&self) -> Result<()> {
let mut ssh = self.connect()?;
crate::install::verify_installation(&mut ssh, &self.config).await
}
pub async fn upgrade(&self, new_version: &str) -> Result<()> {
let mut ssh = self.connect()?;
let current_version = operations::get_installed_version(&mut ssh)
.await?
.ok_or(Error::NotInstalled)?;
crate::install::upgrade(&mut ssh, ¤t_version, new_version, &self.config).await
}
pub async fn backup_config(&self) -> Result<crate::backup::ConfigBackup> {
let mut ssh = self.connect()?;
crate::backup::backup_config(&mut ssh, &self.config).await
}
pub async fn list_backups(&self) -> Result<Vec<crate::backup::ConfigBackup>> {
let mut ssh = self.connect()?;
crate::backup::list_backups(&mut ssh).await
}
pub async fn restore_backup(&self, backup: &crate::backup::ConfigBackup) -> Result<()> {
let mut ssh = self.connect()?;
crate::backup::restore_backup(&mut ssh, &self.config, backup).await
}
pub async fn rollback_config(&self) -> Result<()> {
let mut ssh = self.connect()?;
crate::backup::rollback_config(&mut ssh, &self.config).await
}
pub async fn cleanup_old_backups(&self, keep_count: usize) -> Result<usize> {
let mut ssh = self.connect()?;
crate::backup::cleanup_old_backups(&mut ssh, keep_count).await
}
pub async fn read_pg_hba(&self) -> Result<String> {
let mut ssh = self.connect()?;
crate::backup::read_pg_hba(&mut ssh, &self.config).await
}
pub async fn dry_run_configure(&self) -> Result<ConfigDiff> {
info!("Running dry-run configuration check (no changes will be applied)");
self.diff().await
}
pub async fn apply_diff_safe(&self, diff: &ConfigDiff) -> Result<()> {
if !diff.has_changes() {
info!("No changes to apply");
return Ok(());
}
info!("Creating backup before applying changes...");
let backup = self.backup_config().await?;
info!("✓ Backup created: {}", backup.backup_dir);
info!("Applying configuration changes...");
match self.apply_diff(diff).await {
Ok(_) => {
info!("✓ Configuration changes applied successfully");
Ok(())
}
Err(e) => {
warn!("Configuration apply failed, rolling back to backup...");
self.restore_backup(&backup).await?;
info!("✓ Configuration rolled back successfully");
Err(e)
}
}
}
pub async fn list_users(&self) -> Result<Vec<crate::user_db_management::UserInfo>> {
let mut ssh = self.connect()?;
crate::user_db_management::list_users(&mut ssh).await
}
pub async fn list_databases(&self) -> Result<Vec<crate::user_db_management::DatabaseInfo>> {
let mut ssh = self.connect()?;
crate::user_db_management::list_databases(&mut ssh).await
}
pub async fn drop_database(&self, database_name: &str) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::drop_database(&mut ssh, database_name).await
}
pub async fn drop_user(&self, username: &str) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::drop_user(&mut ssh, username).await
}
pub async fn update_user_password(&self, username: &str, new_password: &str) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::update_user_password(&mut ssh, username, new_password).await
}
pub async fn grant_privileges(
&self,
database: &str,
username: &str,
privileges: &[crate::user_db_management::Privilege],
) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::grant_privileges(&mut ssh, database, username, privileges).await
}
pub async fn revoke_privileges(
&self,
database: &str,
username: &str,
privileges: &[crate::user_db_management::Privilege],
) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::revoke_privileges(&mut ssh, database, username, privileges).await
}
pub async fn create_role(
&self,
role_name: &str,
can_login: bool,
is_superuser: bool,
) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::create_role(&mut ssh, role_name, can_login, is_superuser).await
}
pub async fn grant_role(&self, role_name: &str, username: &str) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::grant_role(&mut ssh, role_name, username).await
}
pub async fn revoke_role(&self, role_name: &str, username: &str) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::revoke_role(&mut ssh, role_name, username).await
}
pub async fn user_exists(&self, username: &str) -> Result<bool> {
let mut ssh = self.connect()?;
crate::user_db_management::user_exists(&mut ssh, username).await
}
pub async fn database_exists(&self, database: &str) -> Result<bool> {
let mut ssh = self.connect()?;
crate::user_db_management::database_exists(&mut ssh, database).await
}
pub async fn create_database_with_options(
&self,
database_name: &str,
owner: Option<&str>,
encoding: Option<&str>,
template: Option<&str>,
) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::create_database_with_options(
&mut ssh,
database_name,
owner,
encoding,
template,
)
.await
}
pub async fn create_user_with_options(
&self,
username: &str,
password: &str,
is_superuser: bool,
can_create_db: bool,
can_create_role: bool,
connection_limit: Option<i32>,
) -> Result<()> {
let mut ssh = self.connect()?;
crate::user_db_management::create_user_with_options(
&mut ssh,
username,
password,
is_superuser,
can_create_db,
can_create_role,
connection_limit,
)
.await
}
}
#[derive(Debug, Default)]
pub struct PostgresManagerBuilder {
config: Option<PostgresConfig>,
server_ip: Option<String>,
ssh_user: Option<String>,
ssh_password: Option<String>,
ssh_key_path: Option<String>,
ssh_port: Option<u16>,
private_ip: Option<String>,
}
impl PostgresManagerBuilder {
pub fn config(mut self, config: PostgresConfig) -> Self {
self.config = Some(config);
self
}
pub fn server_ip(mut self, ip: impl Into<String>) -> Self {
self.server_ip = Some(ip.into());
self
}
pub fn ssh_user(mut self, user: impl Into<String>) -> Self {
self.ssh_user = Some(user.into());
self
}
pub fn ssh_password(mut self, password: impl Into<String>) -> Self {
self.ssh_password = Some(password.into());
self
}
pub fn ssh_key_path(mut self, path: impl Into<String>) -> Self {
self.ssh_key_path = Some(path.into());
self
}
pub fn ssh_port(mut self, port: u16) -> Self {
self.ssh_port = Some(port);
self
}
pub fn private_ip(mut self, ip: impl Into<String>) -> Self {
self.private_ip = Some(ip.into());
self
}
pub fn build(self) -> Result<PostgresManager> {
Ok(PostgresManager {
config: self
.config
.ok_or_else(|| Error::MissingConfig("config".to_string()))?,
server_ip: self
.server_ip
.ok_or_else(|| Error::MissingConfig("server_ip".to_string()))?,
ssh_user: self.ssh_user.unwrap_or_else(|| "root".to_string()),
ssh_password: self.ssh_password,
ssh_key_path: self.ssh_key_path,
ssh_port: self.ssh_port.unwrap_or(22),
private_ip: self.private_ip,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> PostgresConfig {
PostgresConfig::builder()
.version("15")
.database_name("test_db")
.username("test_user")
.password("test_pass")
.build()
.unwrap()
}
#[test]
fn test_builder_minimal() {
let config = create_test_config();
let manager = PostgresManager::builder()
.config(config.clone())
.server_ip("192.168.1.100")
.build()
.unwrap();
assert_eq!(manager.server_ip(), "192.168.1.100");
assert_eq!(manager.ssh_user(), "root");
assert_eq!(manager.ssh_port, 22);
assert_eq!(manager.private_ip(), None);
}
#[test]
fn test_builder_full() {
let config = create_test_config();
let manager = PostgresManager::builder()
.config(config.clone())
.server_ip("192.168.1.100")
.ssh_user("admin")
.ssh_port(2222)
.private_ip("10.0.1.100")
.build()
.unwrap();
assert_eq!(manager.server_ip(), "192.168.1.100");
assert_eq!(manager.ssh_user(), "admin");
assert_eq!(manager.ssh_port, 2222);
assert_eq!(manager.private_ip(), Some("10.0.1.100"));
}
#[test]
fn test_builder_missing_config() {
let result = PostgresManager::builder()
.server_ip("192.168.1.100")
.build();
assert!(matches!(result, Err(Error::MissingConfig(_))));
}
#[test]
fn test_builder_missing_server_ip() {
let config = create_test_config();
let result = PostgresManager::builder().config(config).build();
assert!(matches!(result, Err(Error::MissingConfig(_))));
}
}