lmrc-postgres 0.3.16

PostgreSQL management library for the LMRC Stack - comprehensive library for managing PostgreSQL installations on remote servers via SSH
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
//! Advanced installation features
//!
//! This module provides comprehensive installation features including:
//! - System requirements checking
//! - Version availability verification
//! - Progress reporting
//! - Rollback on failure
//! - Upgrade/downgrade support

use crate::config::PostgresConfig;
use crate::error::{Error, Result};
use lmrc_ssh::SshClient;
use tracing::{debug, info, warn};

/// Platform information
#[derive(Debug, Clone, PartialEq)]
pub enum Platform {
    /// Debian-based systems
    Debian,
    /// Ubuntu-based systems
    Ubuntu,
    /// RedHat-based systems
    RedHat,
    /// CentOS
    CentOS,
    /// Alpine Linux
    Alpine,
    /// Unknown platform
    Unknown(String),
}

impl Platform {
    /// Get platform name as string
    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,
        }
    }

    /// Check if platform is supported
    pub fn is_supported(&self) -> bool {
        matches!(self, Platform::Debian | Platform::Ubuntu)
    }
}

/// System requirements
#[derive(Debug, Clone)]
pub struct SystemRequirements {
    /// Minimum RAM in MB
    pub min_ram_mb: u64,
    /// Minimum free disk space in MB
    pub min_disk_mb: u64,
    /// Minimum CPU cores
    pub min_cpu_cores: u32,
}

impl Default for SystemRequirements {
    fn default() -> Self {
        Self {
            min_ram_mb: 1024,  // 1GB RAM
            min_disk_mb: 5120, // 5GB disk
            min_cpu_cores: 1,
        }
    }
}

/// System information
#[derive(Debug, Clone)]
pub struct SystemInfo {
    /// Platform
    pub platform: Platform,
    /// OS version
    pub os_version: String,
    /// Total RAM in MB
    pub total_ram_mb: u64,
    /// Free disk space in MB
    pub free_disk_mb: u64,
    /// Number of CPU cores
    pub cpu_cores: u32,
}

/// Installation progress callback
pub type ProgressCallback = Box<dyn Fn(InstallationStep, u8) + Send + Sync>;

/// Installation step
#[derive(Debug, Clone, PartialEq)]
pub enum InstallationStep {
    /// Checking system requirements
    CheckingRequirements,
    /// Detecting platform
    DetectingPlatform,
    /// Checking version availability
    CheckingVersionAvailability,
    /// Installing prerequisites
    InstallingPrerequisites,
    /// Adding repository
    AddingRepository,
    /// Updating package list
    UpdatingPackages,
    /// Installing PostgreSQL
    InstallingPostgres,
    /// Starting service
    StartingService,
    /// Verifying installation
    VerifyingInstallation,
    /// Installation complete
    Complete,
}

impl InstallationStep {
    /// Get step description
    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",
        }
    }
}

/// Detect the operating system platform
pub async fn detect_platform(ssh: &mut SshClient) -> Result<Platform> {
    debug!("Detecting platform");

    // Try to read /etc/os-release
    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);
        }
    }

    // Fallback: check for specific files
    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()))
}

/// Get system information
pub async fn get_system_info(ssh: &mut SshClient) -> Result<SystemInfo> {
    debug!("Gathering system information");

    let platform = detect_platform(ssh).await?;

    // Get OS version
    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());

    // Get total RAM in MB
    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);

    // Get free disk space in MB (root partition)
    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);

    // Get CPU cores
    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,
    })
}

/// Check system requirements
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);

    // Check platform support
    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()
        )));
    }

    // Check RAM
    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
        )));
    }

    // Check disk space
    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
        )));
    }

    // Check CPU cores
    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)
}

/// Check if PostgreSQL version is available
pub async fn check_version_available(ssh: &mut SshClient, version: &str) -> Result<bool> {
    debug!("Checking if PostgreSQL {} is available", version);

    // First ensure repository is added
    let has_repo = ssh
        .execute("test -f /etc/apt/sources.list.d/pgdg.list")
        .is_ok();

    if !has_repo {
        // Add repository temporarily to check
        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();
    }

    // Check if package is available
    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)
        }
    }
}

/// Upgrade PostgreSQL to a new version
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
    );

    // Check if target version is available
    if !check_version_available(ssh, to_version).await? {
        return Err(Error::Installation(format!(
            "PostgreSQL version {} is not available",
            to_version
        )));
    }

    // Stop current service
    info!("Stopping PostgreSQL {}", from_version);
    ssh.execute("systemctl stop postgresql")
        .map_err(|e| Error::ServiceError(format!("Failed to stop service: {}", e)))?;

    // Install new version
    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)))?;

    // Run pg_upgrade (simplified - in production this needs more careful handling)
    info!("Note: Manual data migration may be required");
    warn!(
        "PostgreSQL {} installed. You may need to migrate data from version {}",
        to_version, from_version
    );

    // Start new service
    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(())
}

/// Verify installation comprehensively
pub async fn verify_installation(ssh: &mut SshClient, config: &PostgresConfig) -> Result<()> {
    info!("Performing comprehensive installation verification");

    // 1. Check PostgreSQL is installed
    let psql_check = ssh.execute("which psql");
    if psql_check.is_err() {
        return Err(Error::Installation(
            "PostgreSQL not found in PATH".to_string(),
        ));
    }

    // 2. Check version
    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);

    // 3. Check service is running
    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");

    // 4. Check service is enabled
    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");
    }

    // 5. Check PostgreSQL is listening
    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");
    }

    // 6. Check configuration files exist
    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");

    // 7. Check data directory
    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(())
}

/// Rollback installation on failure
pub async fn rollback_installation(ssh: &mut SshClient, version: &str) -> Result<()> {
    warn!("Rolling back PostgreSQL {} installation", version);

    // Stop service if running
    let _ = ssh.execute("systemctl stop postgresql");

    // Remove package
    let remove_cmd = format!("apt-get remove -y postgresql-{}", version);
    ssh.execute(&remove_cmd)
        .map_err(|e| Error::Uninstallation(format!("Rollback failed: {}", e)))?;

    // Clean up
    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");
    }
}