- Phase 1: Dashboard Widget System (11 widgets) - Phase 2: Resource Tree View (ResourceTree + ResourceFilter) - Phase 3: VM Manager UI (VMList + SnapshotForm + MigrationForm) - Phase 4: Backup Manager UI (BackupJobList) - Phase 5: Ceph Manager UI (CephHealthWidget + PoolList + OSDList + MonitorList) - Phase 6: SDN Manager UI (EVPNZoneList) - Phase 7: Firewall Manager UI (FirewallRuleList) - Phase 8: HA Groups Manager UI (HAGroupsList + HAResourcesList) - Phase 9: User Management UI (RealmList + UserList) - Phase 10: Certificate Manager UI (CertificateList) - Phase 11: Subscription Registry UI (SubscriptionList) All components pass TypeScript, ESLint, and existing tests. All Rust code passes clippy and format checks.
312 lines
9.3 KiB
Rust
312 lines
9.3 KiB
Rust
// Backup management module
|
|
// Provides operations for managing Proxmox Backup Server
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Backup job information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BackupJob {
|
|
pub job_id: u32,
|
|
pub name: String,
|
|
pub schedule: String,
|
|
pub enabled: bool,
|
|
pub datastore: String,
|
|
pub source: String,
|
|
pub retention: String,
|
|
}
|
|
|
|
/// Datastore information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatastoreInfo {
|
|
pub datastore: String,
|
|
pub node: String,
|
|
pub size: u64,
|
|
pub used: u64,
|
|
pub available: u64,
|
|
pub status: String,
|
|
}
|
|
|
|
/// List backup jobs
|
|
pub async fn list_backup_jobs(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
ticket: &str,
|
|
) -> Result<Vec<BackupJob>, String> {
|
|
let path = format!("nodes/{}/backup/jobs", node);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list backup jobs: {}", e))?;
|
|
|
|
if let Some(jobs) = response.get("data").and_then(|d| d.as_array()) {
|
|
let backup_jobs: Vec<BackupJob> = jobs
|
|
.iter()
|
|
.filter_map(|job| {
|
|
let job_id = job.get("jobid")?.as_u64()?;
|
|
let name = job.get("name")?.as_str()?.to_string();
|
|
let schedule = job.get("schedule")?.as_str()?.to_string();
|
|
let enabled = job.get("enabled")?.as_bool()?;
|
|
let datastore = job.get("datastore")?.as_str()?.to_string();
|
|
let source = job.get("source")?.as_str()?.to_string();
|
|
let retention = job.get("retention")?.as_str().unwrap_or("").to_string();
|
|
|
|
Some(BackupJob {
|
|
job_id: job_id as u32,
|
|
name,
|
|
schedule,
|
|
enabled,
|
|
datastore,
|
|
source,
|
|
retention,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(backup_jobs)
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Create backup job
|
|
pub async fn create_backup_job(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
job: &BackupJob,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/backup/jobs", node);
|
|
let config = serde_json::json!({
|
|
"jobid": job.job_id,
|
|
"name": job.name,
|
|
"schedule": job.schedule,
|
|
"enabled": job.enabled,
|
|
"datastore": job.datastore,
|
|
"source": job.source,
|
|
"retention": job.retention
|
|
});
|
|
|
|
let _response: serde_json::Value = client
|
|
.post(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to create backup job {}: {}", job.job_id, e))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Update backup job
|
|
pub async fn update_backup_job(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
job_id: u32,
|
|
job: &BackupJob,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/backup/jobs/{}", node, job_id);
|
|
let config = serde_json::json!({
|
|
"name": job.name,
|
|
"schedule": job.schedule,
|
|
"enabled": job.enabled,
|
|
"datastore": job.datastore,
|
|
"source": job.source,
|
|
"retention": job.retention
|
|
});
|
|
|
|
let _response: serde_json::Value = client
|
|
.put(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to update backup job {}: {}", job_id, e))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete backup job
|
|
pub async fn delete_backup_job(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
job_id: u32,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/backup/jobs/{}", node, job_id);
|
|
let _response: serde_json::Value = client
|
|
.delete(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to delete backup job {}: {}", job_id, e))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Trigger backup job manually
|
|
pub async fn trigger_backup_job(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
job_id: u32,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/backup/jobs/{}/run", node, job_id);
|
|
let _response: serde_json::Value = client
|
|
.post(&path, &serde_json::json!({}), Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to trigger backup job {}: {}", job_id, e))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// List datastores
|
|
pub async fn list_datastores(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
ticket: &str,
|
|
) -> Result<Vec<DatastoreInfo>, String> {
|
|
let path = "datastore";
|
|
let response: serde_json::Value = client
|
|
.get(path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list datastores: {}", e))?;
|
|
|
|
if let Some(datastores) = response.get("data").and_then(|d| d.as_array()) {
|
|
let datastore_list: Vec<DatastoreInfo> = datastores
|
|
.iter()
|
|
.filter_map(|ds| {
|
|
let datastore = ds.get("datastore")?.as_str()?.to_string();
|
|
let node = ds.get("node")?.as_str()?.to_string();
|
|
let size = ds.get("size")?.as_u64()?;
|
|
let used = ds.get("used")?.as_u64()?;
|
|
let available = ds.get("available")?.as_u64()?;
|
|
let status = ds.get("status")?.as_str()?.to_string();
|
|
|
|
Some(DatastoreInfo {
|
|
datastore,
|
|
node,
|
|
size,
|
|
used,
|
|
available,
|
|
status,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(datastore_list)
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Get datastore status
|
|
pub async fn get_datastore_status(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
datastore: &str,
|
|
ticket: &str,
|
|
) -> Result<DatastoreInfo, String> {
|
|
let path = format!("nodes/{}/backup/status?datastore={}", node, datastore);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to get datastore status: {}", e))?;
|
|
|
|
let ds = response.get("data").ok_or("Invalid response format")?;
|
|
|
|
Ok(DatastoreInfo {
|
|
datastore: datastore.to_string(),
|
|
node: node.to_string(),
|
|
size: ds.get("size").and_then(|s| s.as_u64()).unwrap_or(0),
|
|
used: ds.get("used").and_then(|u| u.as_u64()).unwrap_or(0),
|
|
available: ds.get("available").and_then(|a| a.as_u64()).unwrap_or(0),
|
|
status: ds
|
|
.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string(),
|
|
})
|
|
}
|
|
|
|
/// List backup snapshots
|
|
pub async fn list_backup_snapshots(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
datastore: &str,
|
|
ticket: &str,
|
|
) -> Result<Vec<serde_json::Value>, String> {
|
|
let path = format!("nodes/{}/backup/snapshots?datastore={}", node, datastore);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list backup snapshots: {}", e))?;
|
|
|
|
if let Some(snapshots) = response.get("data").and_then(|d| d.as_array()) {
|
|
Ok(snapshots.to_vec())
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Restore backup
|
|
pub async fn restore_backup(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
datastore: &str,
|
|
backup_id: &str,
|
|
target_node: &str,
|
|
target_vmid: u32,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/backup/restore", node);
|
|
let config = serde_json::json!({
|
|
"datastore": datastore,
|
|
"backup": backup_id,
|
|
"target-node": target_node,
|
|
"target-vmid": target_vmid
|
|
});
|
|
|
|
let _response: serde_json::Value =
|
|
client
|
|
.post(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| {
|
|
format!(
|
|
"Failed to restore backup {} to VM {}: {}",
|
|
backup_id, target_vmid, e
|
|
)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_backup_job_serialization() {
|
|
let job = BackupJob {
|
|
job_id: 1,
|
|
name: "daily-backup".to_string(),
|
|
schedule: "0 2 * * *".to_string(),
|
|
enabled: true,
|
|
datastore: "pbs-datastore".to_string(),
|
|
source: "/data".to_string(),
|
|
retention: "30d".to_string(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&job).unwrap();
|
|
let deserialized: BackupJob = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(job.job_id, deserialized.job_id);
|
|
assert_eq!(job.name, deserialized.name);
|
|
assert_eq!(job.enabled, deserialized.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_datastore_info_serialization() {
|
|
let ds = DatastoreInfo {
|
|
datastore: "local".to_string(),
|
|
node: "pbs-node-1".to_string(),
|
|
size: 1000000000000,
|
|
used: 300000000000,
|
|
available: 700000000000,
|
|
status: "available".to_string(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&ds).unwrap();
|
|
let deserialized: DatastoreInfo = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(ds.datastore, deserialized.datastore);
|
|
assert_eq!(ds.status, "available");
|
|
}
|
|
}
|