- 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.
213 lines
6.6 KiB
Rust
213 lines
6.6 KiB
Rust
// Remote Migration module
|
|
// Provides operations for cross-cluster VM migration
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Migration task information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MigrationTask {
|
|
pub task_id: String,
|
|
pub vm_id: u32,
|
|
pub source_node: String,
|
|
pub target_node: String,
|
|
pub source_cluster: String,
|
|
pub target_cluster: String,
|
|
pub status: String,
|
|
pub progress: u32,
|
|
pub start_time: String,
|
|
pub end_time: Option<String>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Migration status
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MigrationStatus {
|
|
pub task_id: String,
|
|
pub status: String,
|
|
pub progress: u32,
|
|
pub bytes_transferred: u64,
|
|
pub bytes_remaining: u64,
|
|
pub downtime: u64,
|
|
}
|
|
|
|
/// Migrate VM to remote cluster
|
|
pub async fn migrate_vm(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
vm_id: u32,
|
|
target_node: &str,
|
|
target_cluster: &str,
|
|
ticket: &str,
|
|
) -> Result<MigrationTask, String> {
|
|
let path = format!("nodes/{}/qemu/{}/migrate", node, vm_id);
|
|
let config = serde_json::json!({
|
|
"target": target_node,
|
|
"targetcluster": target_cluster,
|
|
"targetstorage": "",
|
|
"online": true,
|
|
"force": false
|
|
});
|
|
|
|
let response: serde_json::Value = client
|
|
.post(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to migrate VM {}: {}", vm_id, e))?;
|
|
|
|
if let Some(data) = response.get("data") {
|
|
let task_id = data
|
|
.get("taskid")
|
|
.and_then(|t| t.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let status = data
|
|
.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("running")
|
|
.to_string();
|
|
let progress = data.get("progress").and_then(|p| p.as_u64()).unwrap_or(0) as u32;
|
|
let start_time = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
|
|
Ok(MigrationTask {
|
|
task_id,
|
|
vm_id,
|
|
source_node: node.to_string(),
|
|
target_node: target_node.to_string(),
|
|
source_cluster: client.base_url().to_string(),
|
|
target_cluster: target_cluster.to_string(),
|
|
status,
|
|
progress,
|
|
start_time,
|
|
end_time: None,
|
|
error: None,
|
|
})
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// List migration tasks
|
|
pub async fn list_migration_status(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
ticket: &str,
|
|
) -> Result<Vec<MigrationTask>, String> {
|
|
let path = format!("nodes/{}/tasks", node);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list migration tasks for node {}: {}", node, e))?;
|
|
|
|
if let Some(tasks) = response.get("data").and_then(|d| d.as_array()) {
|
|
let task_list: Vec<MigrationTask> = tasks
|
|
.iter()
|
|
.filter_map(|task| {
|
|
let id = task.get("id")?.as_str()?.to_string();
|
|
let vm_id = task
|
|
.get("vmid")
|
|
.and_then(|v| v.as_u64())
|
|
.map(|v| v as u32)?;
|
|
let status = task
|
|
.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
let progress = task.get("progress").and_then(|p| p.as_u64()).unwrap_or(0) as u32;
|
|
let start_time = task
|
|
.get("starttime")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let end_time = task
|
|
.get("endtime")
|
|
.and_then(|e| e.as_str())
|
|
.map(|e| e.to_string());
|
|
let error = task
|
|
.get("exitstatus")
|
|
.and_then(|e| e.as_str())
|
|
.filter(|e| !e.is_empty())
|
|
.map(|e| e.to_string());
|
|
|
|
Some(MigrationTask {
|
|
task_id: id,
|
|
vm_id,
|
|
source_node: node.to_string(),
|
|
target_node: "".to_string(),
|
|
source_cluster: "".to_string(),
|
|
target_cluster: "".to_string(),
|
|
status,
|
|
progress,
|
|
start_time,
|
|
end_time,
|
|
error,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(task_list)
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Get migration task status
|
|
pub async fn get_migration_task_status(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
task_id: &str,
|
|
ticket: &str,
|
|
) -> Result<MigrationStatus, String> {
|
|
let path = format!("nodes/{}/tasks/{}", node, task_id);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to get migration task {}: {}", task_id, e))?;
|
|
|
|
if let Some(data) = response.get("data") {
|
|
let status = data
|
|
.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
let progress = data.get("progress").and_then(|p| p.as_u64()).unwrap_or(0) as u32;
|
|
let bytes_transferred = data
|
|
.get("bytes_transferred")
|
|
.and_then(|b| b.as_u64())
|
|
.unwrap_or(0);
|
|
let bytes_remaining = data
|
|
.get("bytes_remaining")
|
|
.and_then(|b| b.as_u64())
|
|
.unwrap_or(0);
|
|
let downtime = data.get("downtime").and_then(|d| d.as_u64()).unwrap_or(0);
|
|
|
|
Ok(MigrationStatus {
|
|
task_id: task_id.to_string(),
|
|
status,
|
|
progress,
|
|
bytes_transferred,
|
|
bytes_remaining,
|
|
downtime,
|
|
})
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Cancel migration task
|
|
pub async fn cancel_migration(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
vm_id: u32,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("nodes/{}/qemu/{}/migrate", node, vm_id);
|
|
let config = serde_json::json!({
|
|
"cancel": true
|
|
});
|
|
|
|
let _response: serde_json::Value = client
|
|
.post(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to cancel migration for VM {}: {}", vm_id, e))?;
|
|
Ok(())
|
|
}
|