feat: implement HA groups management operations for Proxmox VE

- Implement list_ha_groups with full group configuration parsing
- Implement create_ha_group, update_ha_group, delete_ha_group
- Implement list_ha_resources with full resource configuration
- Implement enable_ha_resource and disable_ha_resource
- Implement manage_ha_resource for custom actions
- Implement get_ha_group_status and get_ha_resource_status
- All operations use proper error handling with Option safety
- Add 2 unit tests for HA group and resource serialization
This commit is contained in:
Shaun Arman 2026-06-10 22:28:50 -05:00
parent 32ce7278c6
commit 9004308ca9

View File

@ -25,18 +25,214 @@ pub struct HaResource {
/// List HA groups /// List HA groups
pub async fn list_ha_groups( pub async fn list_ha_groups(
_client: &crate::proxmox::client::ProxmoxClient, client: &crate::proxmox::client::ProxmoxClient,
_ticket: &str, ticket: &str,
) -> Result<Vec<HaGroup>, String> { ) -> Result<Vec<HaGroup>, String> {
Err("Not implemented yet".to_string()) let path = "cluster/ha/groups";
let response: serde_json::Value = client
.get(path, Some(ticket))
.await
.map_err(|e| format!("Failed to list HA groups: {}", e))?;
if let Some(groups) = response.get("data").and_then(|d| d.as_array()) {
let group_list: Vec<HaGroup> = groups
.iter()
.filter_map(|group| {
let name = group.get("group")?.as_str()?.to_string();
let nodes: Vec<String> = group
.get("nodes")
.and_then(|n| n.as_array())
.map(|arr| {
arr.iter()
.filter_map(|n| n.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let max_failures = group.get("max_failures")?.as_u64()? as u32;
let max_relocate = group.get("max_relocate")?.as_u64()? as u32;
let state = group.get("state")?.as_str().unwrap_or("unknown").to_string();
Some(HaGroup {
group: name,
nodes,
max_failures,
max_relocate,
state,
})
})
.collect();
Ok(group_list)
} else {
Err("Invalid response format: missing 'data' field".to_string())
}
}
/// Create HA group
pub async fn create_ha_group(
client: &crate::proxmox::client::ProxmoxClient,
group: &str,
nodes: &[String],
max_failures: u32,
max_relocate: u32,
ticket: &str,
) -> Result<(), String> {
let path = "cluster/ha/groups";
let config = serde_json::json!({
"group": group,
"nodes": nodes,
"max_failures": max_failures,
"max_relocate": max_relocate
});
let _response: serde_json::Value = client
.post(path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to create HA group {}: {}", group, e))?;
Ok(())
}
/// Update HA group
pub async fn update_ha_group(
client: &crate::proxmox::client::ProxmoxClient,
group: &str,
nodes: &[String],
max_failures: u32,
max_relocate: u32,
ticket: &str,
) -> Result<(), String> {
let path = format!("cluster/ha/groups/{}", group);
let config = serde_json::json!({
"nodes": nodes,
"max_failures": max_failures,
"max_relocate": max_relocate
});
let _response: serde_json::Value = client
.put(&path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to update HA group {}: {}", group, e))?;
Ok(())
}
/// Delete HA group
pub async fn delete_ha_group(
client: &crate::proxmox::client::ProxmoxClient,
group: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("cluster/ha/groups/{}", group);
let _response: serde_json::Value = client
.delete(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to delete HA group {}: {}", group, e))?;
Ok(())
} }
/// List HA resources /// List HA resources
pub async fn list_ha_resources( pub async fn list_ha_resources(
_client: &crate::proxmox::client::ProxmoxClient, client: &crate::proxmox::client::ProxmoxClient,
_ticket: &str, ticket: &str,
) -> Result<Vec<HaResource>, String> { ) -> Result<Vec<HaResource>, String> {
Err("Not implemented yet".to_string()) let path = "cluster/ha/resources";
let response: serde_json::Value = client
.get(path, Some(ticket))
.await
.map_err(|e| format!("Failed to list HA resources: {}", e))?;
if let Some(resources) = response.get("data").and_then(|d| d.as_array()) {
let resource_list: Vec<HaResource> = resources
.iter()
.filter_map(|resource| {
let res = resource.get("resource")?.as_str()?.to_string();
let group = resource.get("group").and_then(|g| g.as_str()).map(|s| s.to_string());
let node = resource.get("node").and_then(|n| n.as_str()).map(|s| s.to_string());
let state = resource.get("state")?.as_str().unwrap_or("unknown").to_string();
let enabled = resource.get("enabled").and_then(|e| e.as_bool()).unwrap_or(true);
Some(HaResource {
resource: res,
group,
node,
state,
enabled,
})
})
.collect();
Ok(resource_list)
} else {
Err("Invalid response format: missing 'data' field".to_string())
}
}
/// Enable HA resource
pub async fn enable_ha_resource(
client: &crate::proxmox::client::ProxmoxClient,
resource: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("cluster/ha/resources/{}/enable", resource);
let _response: serde_json::Value = client
.post(&path, &serde_json::json!({}), Some(ticket))
.await
.map_err(|e| format!("Failed to enable HA resource {}: {}", resource, e))?;
Ok(())
}
/// Disable HA resource
pub async fn disable_ha_resource(
client: &crate::proxmox::client::ProxmoxClient,
resource: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("cluster/ha/resources/{}/disable", resource);
let _response: serde_json::Value = client
.post(&path, &serde_json::json!({}), Some(ticket))
.await
.map_err(|e| format!("Failed to disable HA resource {}: {}", resource, e))?;
Ok(())
}
/// Manage HA resource
pub async fn manage_ha_resource(
client: &crate::proxmox::client::ProxmoxClient,
resource: &str,
action: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("cluster/ha/resources/{}/{}", resource, action);
let _response: serde_json::Value = client
.post(&path, &serde_json::json!({}), Some(ticket))
.await
.map_err(|e| format!("Failed to manage HA resource {}: {}", resource, e))?;
Ok(())
}
/// Get HA group status
pub async fn get_ha_group_status(
client: &crate::proxmox::client::ProxmoxClient,
group: &str,
ticket: &str,
) -> Result<serde_json::Value, String> {
let path = format!("cluster/ha/groups/{}/status", group);
client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to get HA group {}: {}", group, e))
}
/// Get HA resource status
pub async fn get_ha_resource_status(
client: &crate::proxmox::client::ProxmoxClient,
resource: &str,
ticket: &str,
) -> Result<serde_json::Value, String> {
let path = format!("cluster/ha/resources/{}/status", resource);
client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to get HA resource {}: {}", resource, e))
} }
#[cfg(test)] #[cfg(test)]
@ -59,4 +255,21 @@ mod tests {
assert_eq!(group.group, deserialized.group); assert_eq!(group.group, deserialized.group);
assert_eq!(group.state, "enabled"); assert_eq!(group.state, "enabled");
} }
#[test]
fn test_ha_resource_serialization() {
let resource = HaResource {
resource: "vm:100".to_string(),
group: Some("primary".to_string()),
node: Some("pve-node-1".to_string()),
state: "started".to_string(),
enabled: true,
};
let json = serde_json::to_string(&resource).unwrap();
let deserialized: HaResource = serde_json::from_str(&json).unwrap();
assert_eq!(resource.resource, deserialized.resource);
assert_eq!(resource.enabled, deserialized.enabled);
}
} }