- Display single number if all services have same count - Display min-max range if counts differ (indicates problem)
235 lines
8.2 KiB
Rust
235 lines
8.2 KiB
Rust
use async_trait::async_trait;
|
|
use cm_dashboard_shared::{AgentData, BackupData, BackupDiskData, Status};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use tracing::{debug, warn};
|
|
|
|
use super::{Collector, CollectorError};
|
|
|
|
/// Backup collector that reads backup status from TOML files with structured data output
|
|
pub struct BackupCollector {
|
|
/// Directory containing backup status files
|
|
status_dir: String,
|
|
}
|
|
|
|
impl BackupCollector {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
status_dir: "/var/lib/backup/status".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Scan directory for all backup status files
|
|
async fn scan_status_files(&self) -> Result<Vec<PathBuf>, CollectorError> {
|
|
let status_path = Path::new(&self.status_dir);
|
|
|
|
if !status_path.exists() {
|
|
debug!("Backup status directory not found: {}", self.status_dir);
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut status_files = Vec::new();
|
|
|
|
match fs::read_dir(status_path) {
|
|
Ok(entries) => {
|
|
for entry in entries {
|
|
if let Ok(entry) = entry {
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
|
if filename.starts_with("backup-status-") && filename.ends_with(".toml") {
|
|
status_files.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to read backup status directory: {}", e);
|
|
return Ok(Vec::new());
|
|
}
|
|
}
|
|
|
|
Ok(status_files)
|
|
}
|
|
|
|
/// Read a single backup status file
|
|
async fn read_status_file(&self, path: &Path) -> Result<BackupStatusToml, CollectorError> {
|
|
let content = fs::read_to_string(path)
|
|
.map_err(|e| CollectorError::SystemRead {
|
|
path: path.to_string_lossy().to_string(),
|
|
error: e.to_string(),
|
|
})?;
|
|
|
|
let status: BackupStatusToml = toml::from_str(&content)
|
|
.map_err(|e| CollectorError::Parse {
|
|
value: content.clone(),
|
|
error: format!("Failed to parse backup status TOML: {}", e),
|
|
})?;
|
|
|
|
Ok(status)
|
|
}
|
|
|
|
/// Calculate backup status from TOML status field
|
|
fn calculate_backup_status(status_str: &str) -> Status {
|
|
match status_str.to_lowercase().as_str() {
|
|
"success" => Status::Ok,
|
|
"warning" => Status::Warning,
|
|
"failed" | "error" => Status::Critical,
|
|
_ => Status::Unknown,
|
|
}
|
|
}
|
|
|
|
/// Calculate usage status from disk usage percentage
|
|
fn calculate_usage_status(usage_percent: f32) -> Status {
|
|
if usage_percent < 80.0 {
|
|
Status::Ok
|
|
} else if usage_percent < 90.0 {
|
|
Status::Warning
|
|
} else {
|
|
Status::Critical
|
|
}
|
|
}
|
|
|
|
/// Convert BackupStatusToml to BackupData and populate AgentData
|
|
async fn populate_backup_data(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> {
|
|
let status_files = self.scan_status_files().await?;
|
|
|
|
if status_files.is_empty() {
|
|
debug!("No backup status files found");
|
|
agent_data.backup = BackupData {
|
|
repositories: Vec::new(),
|
|
repository_status: Status::Unknown,
|
|
disks: Vec::new(),
|
|
};
|
|
return Ok(());
|
|
}
|
|
|
|
let mut all_repositories = HashSet::new();
|
|
let mut disks = Vec::new();
|
|
let mut worst_status = Status::Ok;
|
|
|
|
for status_file in status_files {
|
|
match self.read_status_file(&status_file).await {
|
|
Ok(backup_status) => {
|
|
// Collect all service names
|
|
for service_name in backup_status.services.keys() {
|
|
all_repositories.insert(service_name.clone());
|
|
}
|
|
|
|
// Calculate backup status
|
|
let backup_status_enum = Self::calculate_backup_status(&backup_status.status);
|
|
|
|
// Calculate usage status from disk space
|
|
let (usage_percent, used_gb, total_gb, usage_status) = if let Some(disk_space) = &backup_status.disk_space {
|
|
let usage_pct = disk_space.usage_percent as f32;
|
|
(
|
|
usage_pct,
|
|
disk_space.used_gb as f32,
|
|
disk_space.total_gb as f32,
|
|
Self::calculate_usage_status(usage_pct),
|
|
)
|
|
} else {
|
|
(0.0, 0.0, 0.0, Status::Unknown)
|
|
};
|
|
|
|
// Update worst status
|
|
worst_status = worst_status.max(backup_status_enum).max(usage_status);
|
|
|
|
// Build service list for this disk
|
|
let services: Vec<String> = backup_status.services.keys().cloned().collect();
|
|
|
|
// Get min and max archive counts to detect inconsistencies
|
|
let archives_min: i64 = backup_status.services.values()
|
|
.map(|service| service.archive_count)
|
|
.min()
|
|
.unwrap_or(0);
|
|
|
|
let archives_max: i64 = backup_status.services.values()
|
|
.map(|service| service.archive_count)
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
// Create disk data
|
|
let disk_data = BackupDiskData {
|
|
serial: backup_status.disk_serial_number.unwrap_or_else(|| "Unknown".to_string()),
|
|
product_name: backup_status.disk_product_name,
|
|
wear_percent: backup_status.disk_wear_percent,
|
|
temperature_celsius: None, // Not available in current TOML
|
|
last_backup_time: Some(backup_status.start_time),
|
|
backup_status: backup_status_enum,
|
|
disk_usage_percent: usage_percent,
|
|
disk_used_gb: used_gb,
|
|
disk_total_gb: total_gb,
|
|
usage_status,
|
|
services,
|
|
archives_min,
|
|
archives_max,
|
|
};
|
|
|
|
disks.push(disk_data);
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to read backup status file {:?}: {}", status_file, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let repositories: Vec<String> = all_repositories.into_iter().collect();
|
|
|
|
agent_data.backup = BackupData {
|
|
repositories,
|
|
repository_status: worst_status,
|
|
disks,
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Collector for BackupCollector {
|
|
async fn collect_structured(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> {
|
|
debug!("Collecting backup status");
|
|
self.populate_backup_data(agent_data).await
|
|
}
|
|
}
|
|
|
|
/// TOML structure for backup status file
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct BackupStatusToml {
|
|
pub backup_name: String,
|
|
pub start_time: String,
|
|
pub current_time: String,
|
|
pub duration_seconds: i64,
|
|
pub status: String,
|
|
pub last_updated: String,
|
|
pub disk_space: Option<DiskSpace>,
|
|
pub disk_product_name: Option<String>,
|
|
pub disk_serial_number: Option<String>,
|
|
pub disk_wear_percent: Option<f32>,
|
|
pub services: HashMap<String, ServiceStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct DiskSpace {
|
|
pub total_bytes: u64,
|
|
pub used_bytes: u64,
|
|
pub available_bytes: u64,
|
|
pub total_gb: f64,
|
|
pub used_gb: f64,
|
|
pub available_gb: f64,
|
|
pub usage_percent: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct ServiceStatus {
|
|
pub status: String,
|
|
pub exit_code: i64,
|
|
pub repo_path: String,
|
|
pub archive_count: i64,
|
|
pub repo_size_bytes: u64,
|
|
} |