diff --git a/src-tauri/src/proxmox/ceph.rs b/src-tauri/src/proxmox/ceph.rs index 31b5b340..af7be2a7 100644 --- a/src-tauri/src/proxmox/ceph.rs +++ b/src-tauri/src/proxmox/ceph.rs @@ -46,45 +46,442 @@ pub struct CephHealth { /// List Ceph pools pub async fn list_pools( - _client: &crate::proxmox::client::ProxmoxClient, - _ticket: &str, + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, ) -> Result, String> { - Err("Not implemented yet".to_string()) + let path = "cluster/ceph/pool"; + let response: serde_json::Value = client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to list Ceph pools: {}", e))?; + + if let Some(pools) = response.get("data").and_then(|d| d.as_array()) { + let pool_list: Vec = pools + .iter() + .filter_map(|pool| { + let pool_name = pool.get("pool")?.as_str()?.to_string(); + let pool_id = pool.get("poolid")?.as_u64()?; + let size = pool.get("size")?.as_u64()? as u32; + let min_size = pool.get("min_size")?.as_u64()? as u32; + let pg_num = pool.get("pg_num")?.as_u64()? as u32; + let used = pool.get("used")?.as_u64()?; + let avail = pool.get("avail")?.as_u64()?; + let status = pool.get("status")?.as_str().unwrap_or("unknown").to_string(); + + Some(CephPool { + pool: pool_name, + pool_id, + size, + min_size, + pg_num, + used, + avail, + status, + }) + }) + .collect(); + + Ok(pool_list) + } else { + Err("Invalid response format: missing 'data' field".to_string()) + } } /// Create Ceph pool pub async fn create_pool( - _client: &crate::proxmox::client::ProxmoxClient, - _pool: &str, - _pg_num: u32, - _ticket: &str, + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + pg_num: u32, + ticket: &str, ) -> Result<(), String> { - Err("Not implemented yet".to_string()) + let path = "cluster/ceph/pool"; + let config = serde_json::json!({ + "pool": pool, + "pg_num": pg_num + }); + + let _response: serde_json::Value = client + .post(path, &config, Some(ticket)) + .await + .map_err(|e| format!("Failed to create Ceph pool {}: {}", pool, e))?; + Ok(()) } /// Delete Ceph pool pub async fn delete_pool( - _client: &crate::proxmox::client::ProxmoxClient, - _pool: &str, - _ticket: &str, + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + ticket: &str, ) -> Result<(), String> { - Err("Not implemented yet".to_string()) + let path = format!("cluster/ceph/pool/{}", pool); + let _response: serde_json::Value = client + .delete(&path, Some(ticket)) + .await + .map_err(|e| format!("Failed to delete Ceph pool {}: {}", pool, e))?; + Ok(()) +} + +/// Set Ceph pool quota +pub async fn set_pool_quota( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + max_bytes: u64, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}", pool); + let config = serde_json::json!({ + "max_bytes": max_bytes + }); + + let _response: serde_json::Value = client + .put(&path, &config, Some(ticket)) + .await + .map_err(|e| format!("Failed to set quota for Ceph pool {}: {}", pool, e))?; + Ok(()) } /// List Ceph OSDs pub async fn list_osds( - _client: &crate::proxmox::client::ProxmoxClient, - _ticket: &str, + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, ) -> Result, String> { - Err("Not implemented yet".to_string()) + let path = "cluster/ceph/osd"; + let response: serde_json::Value = client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to list Ceph OSDs: {}", e))?; + + if let Some(osds) = response.get("data").and_then(|d| d.as_array()) { + let osd_list: Vec = osds + .iter() + .filter_map(|osd| { + let osd_id = osd.get("osd")?.as_u64()? as u32; + let up = osd.get("up")?.as_bool()?; + let in_ = osd.get("in")?.as_bool()?; + let weight = osd.get("weight")?.as_f64()?; + let pg_num = osd.get("pg_num")?.as_u64()? as u32; + let usage = osd.get("kb_used")?.as_f64().unwrap_or(0.0); + + Some(CephOsd { + osd: osd_id, + up, + in_, + weight, + pg_num, + usage, + }) + }) + .collect(); + + Ok(osd_list) + } else { + Err("Invalid response format: missing 'data' field".to_string()) + } +} + +/// Set OSD weight +pub async fn set_osd_weight( + client: &crate::proxmox::client::ProxmoxClient, + osd_id: u32, + weight: f64, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/osd/{}", osd_id); + let config = serde_json::json!({ + "weight": weight + }); + + let _response: serde_json::Value = client + .put(&path, &config, Some(ticket)) + .await + .map_err(|e| format!("Failed to set weight for OSD {}: {}", osd_id, e))?; + Ok(()) +} + +/// Mark OSD out +pub async fn osd_out( + client: &crate::proxmox::client::ProxmoxClient, + osd_id: u32, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/osd/{}/out", osd_id); + let _response: serde_json::Value = client + .post(&path, &serde_json::json!({}), Some(ticket)) + .await + .map_err(|e| format!("Failed to mark OSD {} out: {}", osd_id, e))?; + Ok(()) +} + +/// Mark OSD in +pub async fn osd_in( + client: &crate::proxmox::client::ProxmoxClient, + osd_id: u32, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/osd/{}/in", osd_id); + let _response: serde_json::Value = client + .post(&path, &serde_json::json!({}), Some(ticket)) + .await + .map_err(|e| format!("Failed to mark OSD {} in: {}", osd_id, e))?; + Ok(()) +} + +/// List Ceph MDS +pub async fn list_mds( + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, +) -> Result, String> { + let path = "cluster/ceph/mds"; + let response: serde_json::Value = client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to list Ceph MDS: {}", e))?; + + if let Some(mds) = response.get("data").and_then(|d| d.as_array()) { + Ok(mds.to_vec()) + } else { + Err("Invalid response format: missing 'data' field".to_string()) + } +} + +/// Get MDS status +pub async fn get_mds_status( + client: &crate::proxmox::client::ProxmoxClient, + mds: &str, + ticket: &str, +) -> Result { + let path = format!("cluster/ceph/mds/{}", mds); + client + .get(&path, Some(ticket)) + .await + .map_err(|e| format!("Failed to get MDS {}: {}", mds, e)) +} + +/// Trigger MDS failover +pub async fn mds_failover( + client: &crate::proxmox::client::ProxmoxClient, + mds: &str, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/mds/{}/failover", mds); + let _response: serde_json::Value = client + .post(&path, &serde_json::json!({}), Some(ticket)) + .await + .map_err(|e| format!("Failed to trigger MDS failover {}: {}", mds, e))?; + Ok(()) +} + +/// List RBD images +pub async fn list_rbd( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + ticket: &str, +) -> Result, String> { + let path = format!("cluster/ceph/pool/{}/rbd", pool); + let response: serde_json::Value = client + .get(&path, Some(ticket)) + .await + .map_err(|e| format!("Failed to list RBD images in pool {}: {}", pool, e))?; + + if let Some(images) = response.get("data").and_then(|d| d.as_array()) { + Ok(images.to_vec()) + } else { + Err("Invalid response format: missing 'data' field".to_string()) + } +} + +/// Create RBD image +pub async fn create_rbd( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + image: &str, + size: u64, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}/rbd", pool); + let config = serde_json::json!({ + "image": image, + "size": size + }); + + let _response: serde_json::Value = client + .post(&path, &config, Some(ticket)) + .await + .map_err(|e| format!("Failed to create RBD image {}: {}", image, e))?; + Ok(()) +} + +/// Delete RBD image +pub async fn delete_rbd( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + image: &str, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}/rbd/{}", pool, image); + let _response: serde_json::Value = client + .delete(&path, Some(ticket)) + .await + .map_err(|e| format!("Failed to delete RBD image {}: {}", image, e))?; + Ok(()) +} + +/// Clone RBD image +pub async fn clone_rbd( + client: &crate::proxmox::client::ProxmoxClient, + source_pool: &str, + source_image: &str, + dest_pool: &str, + dest_image: &str, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}/clone", source_pool); + let config = serde_json::json!({ + "source": source_image, + "dest": format!("{}/{}", dest_pool, dest_image) + }); + + let _response: serde_json::Value = client + .post(&path, &config, Some(ticket)) + .await + .map_err(|e| format!( + "Failed to clone RBD image {} to {}/{}: {}", + source_image, dest_pool, dest_image, e + ))?; + Ok(()) +} + +/// Resize RBD image +pub async fn resize_rbd( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + image: &str, + size: u64, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}/rbd/{}/resize", pool, image); + let config = serde_json::json!({ + "size": size + }); + + let _response: serde_json::Value = client + .post(&path, &config, Some(ticket)) + .await + .map_err(|e| format!("Failed to resize RBD image {}: {}", image, e))?; + Ok(()) +} + +/// Create RBD snapshot +pub async fn create_snapshot( + client: &crate::proxmox::client::ProxmoxClient, + pool: &str, + image: &str, + snapshot: &str, + ticket: &str, +) -> Result<(), String> { + let path = format!("cluster/ceph/pool/{}/rbd/{}/snapshot", pool, image); + let config = serde_json::json!({ + "snapshot": snapshot + }); + + let _response: serde_json::Value = client + .post(&path, &config, Some(ticket)) + .await + .map_err(|e| format!( + "Failed to create snapshot {} for RBD image {}: {}", + snapshot, image, e + ))?; + Ok(()) +} + +/// List Ceph monitors +pub async fn list_monitors( + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, +) -> Result, String> { + let path = "cluster/ceph/mon"; + let response: serde_json::Value = client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to list Ceph monitors: {}", e))?; + + if let Some(mons) = response.get("data").and_then(|d| d.as_array()) { + let mon_list: Vec = mons + .iter() + .filter_map(|mon| { + let name = mon.get("name")?.as_str()?.to_string(); + let quorum = mon.get("quorum")?.as_bool()?; + let address = mon.get("addr")?.as_str()?.to_string(); + let version = mon.get("version")?.as_str().unwrap_or("unknown").to_string(); + + Some(CephMonitor { + name, + quorum, + address, + version, + }) + }) + .collect(); + + Ok(mon_list) + } else { + Err("Invalid response format: missing 'data' field".to_string()) + } +} + +/// Get monitor status +pub async fn get_monitor_status( + client: &crate::proxmox::client::ProxmoxClient, + monitor: &str, + ticket: &str, +) -> Result { + let path = format!("cluster/ceph/mon/{}", monitor); + client + .get(&path, Some(ticket)) + .await + .map_err(|e| format!("Failed to get monitor {}: {}", monitor, e)) +} + +/// Get Ceph quorum health +pub async fn quorum_health( + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, +) -> Result { + let path = "cluster/ceph/health"; + client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to get Ceph health: {}", e)) } /// Get Ceph health pub async fn get_ceph_health( - _client: &crate::proxmox::client::ProxmoxClient, - _ticket: &str, + client: &crate::proxmox::client::ProxmoxClient, + ticket: &str, ) -> Result { - Err("Not implemented yet".to_string()) + let path = "cluster/ceph/health"; + let response: serde_json::Value = client + .get(path, Some(ticket)) + .await + .map_err(|e| format!("Failed to get Ceph health: {}", e))?; + + let health = response.get("data").ok_or("Invalid response format")?; + + let details: Vec = health + .get("details") + .and_then(|d| d.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|d| d.get("message").and_then(|m| m.as_str()).map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + + Ok(CephHealth { + status: health.get("status").and_then(|s| s.as_str()).unwrap_or("unknown").to_string(), + summary: health.get("summary").and_then(|s| s.as_str()).unwrap_or("").to_string(), + details, + }) } #[cfg(test)] @@ -110,4 +507,53 @@ mod tests { assert_eq!(pool.pool, deserialized.pool); assert_eq!(pool.status, "healthy"); } + + #[test] + fn test_ceph_osd_serialization() { + let osd = CephOsd { + osd: 0, + up: true, + in_: true, + weight: 1.0, + pg_num: 128, + usage: 0.5, + }; + + let json = serde_json::to_string(&osd).unwrap(); + let deserialized: CephOsd = serde_json::from_str(&json).unwrap(); + + assert_eq!(osd.osd, deserialized.osd); + assert_eq!(osd.up, deserialized.up); + } + + #[test] + fn test_ceph_monitor_serialization() { + let mon = CephMonitor { + name: "pve-mon-1".to_string(), + quorum: true, + address: "10.0.0.1:6789".to_string(), + version: "18.2.0".to_string(), + }; + + let json = serde_json::to_string(&mon).unwrap(); + let deserialized: CephMonitor = serde_json::from_str(&json).unwrap(); + + assert_eq!(mon.name, deserialized.name); + assert_eq!(mon.quorum, deserialized.quorum); + } + + #[test] + fn test_ceph_health_serialization() { + let health = CephHealth { + status: "HEALTH_OK".to_string(), + summary: "Cluster is healthy".to_string(), + details: vec!["All OSDs are up".to_string()], + }; + + let json = serde_json::to_string(&health).unwrap(); + let deserialized: CephHealth = serde_json::from_str(&json).unwrap(); + + assert_eq!(health.status, deserialized.status); + assert_eq!(health.summary, deserialized.summary); + } }