feat: add missing proxmox backend client functions and Rust command stubs
Adds 20 new TypeScript client functions to proxmoxClient.ts with typed interfaces, and 20 corresponding Tauri commands in commands/proxmox.rs wired up across Phases 6-15. All commands are registered in lib.rs. Rust and TypeScript type checks pass clean.
This commit is contained in:
parent
d24f9e2adf
commit
38eecaafcf
File diff suppressed because one or more lines are too long
@ -6451,6 +6451,60 @@
|
||||
"type": "string",
|
||||
"const": "stronghold:deny-save-store-record",
|
||||
"markdownDescription": "Denies the save_store_record command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||
"type": "string",
|
||||
"const": "updater:default",
|
||||
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-check",
|
||||
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download",
|
||||
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download-and-install",
|
||||
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-install",
|
||||
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-check",
|
||||
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download",
|
||||
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download-and-install",
|
||||
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-install",
|
||||
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -6451,6 +6451,60 @@
|
||||
"type": "string",
|
||||
"const": "stronghold:deny-save-store-record",
|
||||
"markdownDescription": "Denies the save_store_record command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||
"type": "string",
|
||||
"const": "updater:default",
|
||||
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-check",
|
||||
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download",
|
||||
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download-and-install",
|
||||
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-install",
|
||||
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-check",
|
||||
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download",
|
||||
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download-and-install",
|
||||
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-install",
|
||||
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -1583,6 +1583,537 @@ pub async fn list_metric_collections(
|
||||
Ok(collections)
|
||||
}
|
||||
|
||||
// ─── Phase 6 - HA Management ──────────────────────────────────────────────────
|
||||
|
||||
/// List HA groups
|
||||
#[tauri::command]
|
||||
pub async fn list_ha_groups(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let groups = crate::proxmox::ha::list_ha_groups(
|
||||
&client_guard,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list HA groups: {}", e))?;
|
||||
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|g| serde_json::to_value(g).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
/// Create HA group
|
||||
#[tauri::command]
|
||||
pub async fn create_ha_group(
|
||||
cluster_id: String,
|
||||
group: String,
|
||||
nodes: Vec<String>,
|
||||
max_failures: u32,
|
||||
max_relocate: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::create_ha_group(
|
||||
&client_guard,
|
||||
&group,
|
||||
&nodes,
|
||||
max_failures,
|
||||
max_relocate,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create HA group: {}", e))
|
||||
}
|
||||
|
||||
/// Update HA group
|
||||
#[tauri::command]
|
||||
pub async fn update_ha_group(
|
||||
cluster_id: String,
|
||||
group: String,
|
||||
nodes: Vec<String>,
|
||||
max_failures: u32,
|
||||
max_relocate: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::update_ha_group(
|
||||
&client_guard,
|
||||
&group,
|
||||
&nodes,
|
||||
max_failures,
|
||||
max_relocate,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update HA group: {}", e))
|
||||
}
|
||||
|
||||
/// Delete HA group
|
||||
#[tauri::command]
|
||||
pub async fn delete_ha_group(
|
||||
cluster_id: String,
|
||||
group: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::delete_ha_group(
|
||||
&client_guard,
|
||||
&group,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete HA group: {}", e))
|
||||
}
|
||||
|
||||
/// List HA resources
|
||||
#[tauri::command]
|
||||
pub async fn list_ha_resources(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let resources = crate::proxmox::ha::list_ha_resources(
|
||||
&client_guard,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list HA resources: {}", e))?;
|
||||
|
||||
resources
|
||||
.into_iter()
|
||||
.map(|r| serde_json::to_value(r).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
/// Enable HA resource
|
||||
#[tauri::command]
|
||||
pub async fn enable_ha_resource(
|
||||
cluster_id: String,
|
||||
resource: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::enable_ha_resource(
|
||||
&client_guard,
|
||||
&resource,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to enable HA resource: {}", e))
|
||||
}
|
||||
|
||||
// ─── Phase 7 - ACL / Users / Realms ──────────────────────────────────────────
|
||||
|
||||
/// List ACL entries
|
||||
#[tauri::command]
|
||||
pub async fn list_acls(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "access/acl";
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list ACLs: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
/// List users
|
||||
#[tauri::command]
|
||||
pub async fn list_users(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "access/users";
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list users: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
/// List authentication realms (typed)
|
||||
#[tauri::command]
|
||||
pub async fn list_realms(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let realms = crate::proxmox::auth_realm::list_auth_realms(
|
||||
&client_guard,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list realms: {}", e))?;
|
||||
|
||||
realms
|
||||
.into_iter()
|
||||
.map(|r| serde_json::to_value(r).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
// ─── Phase 8 - Cluster Notes ──────────────────────────────────────────────────
|
||||
|
||||
/// Get cluster notes
|
||||
#[tauri::command]
|
||||
pub async fn get_cluster_notes(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "cluster/config";
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get cluster notes: {}", e))?;
|
||||
|
||||
Ok(response
|
||||
.get("data")
|
||||
.and_then(|d| d.get("notes"))
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Update cluster notes
|
||||
#[tauri::command]
|
||||
pub async fn update_cluster_notes(
|
||||
cluster_id: String,
|
||||
notes: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "cluster/config";
|
||||
let body = serde_json::json!({ "notes": notes });
|
||||
let _: serde_json::Value = client_guard
|
||||
.put(path, &body, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update cluster notes: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── Phase 9 - Resource Search ────────────────────────────────────────────────
|
||||
|
||||
/// Search Proxmox resources
|
||||
#[tauri::command]
|
||||
pub async fn search_proxmox_resources(
|
||||
cluster_id: String,
|
||||
query: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("cluster/resources?type=vm&search={}", query);
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(&path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to search resources: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
// ─── Phase 10 - Node Status ───────────────────────────────────────────────────
|
||||
|
||||
/// Get node status
|
||||
#[tauri::command]
|
||||
pub async fn get_node_status(
|
||||
cluster_id: String,
|
||||
node_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("nodes/{}/status", node_id);
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(&path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get node status: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.cloned()
|
||||
.ok_or_else(|| "Invalid response format: missing data field".to_string())
|
||||
}
|
||||
|
||||
// ─── Phase 11 - Syslog ────────────────────────────────────────────────────────
|
||||
|
||||
/// Get node syslog
|
||||
#[tauri::command]
|
||||
pub async fn get_syslog(
|
||||
cluster_id: String,
|
||||
node_id: String,
|
||||
limit: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let limit_val = limit.unwrap_or(500);
|
||||
let path = format!("nodes/{}/syslog?limit={}", node_id, limit_val);
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(&path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get syslog: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
// ─── Phase 12 - Network Interfaces ───────────────────────────────────────────
|
||||
|
||||
/// List network interfaces on a node
|
||||
#[tauri::command]
|
||||
pub async fn list_network_interfaces(
|
||||
cluster_id: String,
|
||||
node_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("nodes/{}/network", node_id);
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(&path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list network interfaces: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
// ─── Phase 13 - Cluster Views (typed aliases) ─────────────────────────────────
|
||||
|
||||
/// List cluster views (typed)
|
||||
#[tauri::command]
|
||||
pub async fn list_cluster_views(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let views = crate::proxmox::views::list_views(
|
||||
&client_guard,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list cluster views: {}", e))?;
|
||||
|
||||
views
|
||||
.into_iter()
|
||||
.map(|v| serde_json::to_value(v).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
/// Create cluster view
|
||||
#[tauri::command]
|
||||
pub async fn create_cluster_view(
|
||||
cluster_id: String,
|
||||
view_id: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let view = crate::proxmox::views::DashboardView {
|
||||
view_id,
|
||||
name,
|
||||
description: String::new(),
|
||||
layout: "grid".to_string(),
|
||||
widgets: vec![],
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
updated_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
};
|
||||
|
||||
crate::proxmox::views::add_view(
|
||||
&client_guard,
|
||||
&view,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create cluster view: {}", e))
|
||||
}
|
||||
|
||||
/// Delete cluster view
|
||||
#[tauri::command]
|
||||
pub async fn delete_cluster_view(
|
||||
cluster_id: String,
|
||||
view_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::views::delete_view(
|
||||
&client_guard,
|
||||
&view_id,
|
||||
client_guard.ticket.as_deref().unwrap_or(""),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete cluster view: {}", e))
|
||||
}
|
||||
|
||||
// ─── Phase 14 - Subscription ──────────────────────────────────────────────────
|
||||
|
||||
/// Get subscription status
|
||||
#[tauri::command]
|
||||
pub async fn get_subscription_status(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "nodes/localhost/subscription";
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get subscription status: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.cloned()
|
||||
.ok_or_else(|| "Invalid response format: missing data field".to_string())
|
||||
}
|
||||
|
||||
// ─── Phase 15 - Cluster Task Log ─────────────────────────────────────────────
|
||||
|
||||
/// List cluster-level tasks
|
||||
#[tauri::command]
|
||||
pub async fn list_cluster_tasks(
|
||||
cluster_id: String,
|
||||
limit: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let limit_val = limit.unwrap_or(50);
|
||||
let path = format!("cluster/tasks?limit={}", limit_val);
|
||||
let response: serde_json::Value = client_guard
|
||||
.get(&path, Some(client_guard.ticket.as_deref().unwrap_or("")))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list cluster tasks: {}", e))?;
|
||||
|
||||
response
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -191,6 +191,36 @@ pub fn run() {
|
||||
// Proxmox - Infrastructure (Phase 5)
|
||||
commands::proxmox::get_metrics_summary,
|
||||
commands::proxmox::list_metric_collections,
|
||||
// Proxmox - HA Management (Phase 6)
|
||||
commands::proxmox::list_ha_groups,
|
||||
commands::proxmox::create_ha_group,
|
||||
commands::proxmox::update_ha_group,
|
||||
commands::proxmox::delete_ha_group,
|
||||
commands::proxmox::list_ha_resources,
|
||||
commands::proxmox::enable_ha_resource,
|
||||
// Proxmox - ACL / Users / Realms (Phase 7)
|
||||
commands::proxmox::list_acls,
|
||||
commands::proxmox::list_users,
|
||||
commands::proxmox::list_realms,
|
||||
// Proxmox - Cluster Notes (Phase 8)
|
||||
commands::proxmox::get_cluster_notes,
|
||||
commands::proxmox::update_cluster_notes,
|
||||
// Proxmox - Resource Search (Phase 9)
|
||||
commands::proxmox::search_proxmox_resources,
|
||||
// Proxmox - Node Status (Phase 10)
|
||||
commands::proxmox::get_node_status,
|
||||
// Proxmox - Syslog (Phase 11)
|
||||
commands::proxmox::get_syslog,
|
||||
// Proxmox - Network Interfaces (Phase 12)
|
||||
commands::proxmox::list_network_interfaces,
|
||||
// Proxmox - Cluster Views typed (Phase 13)
|
||||
commands::proxmox::list_cluster_views,
|
||||
commands::proxmox::create_cluster_view,
|
||||
commands::proxmox::delete_cluster_view,
|
||||
// Proxmox - Subscription (Phase 14)
|
||||
commands::proxmox::get_subscription_status,
|
||||
// Proxmox - Cluster Tasks (Phase 15)
|
||||
commands::proxmox::list_cluster_tasks,
|
||||
// Proxmox - Existing
|
||||
commands::proxmox::add_proxmox_cluster,
|
||||
commands::proxmox::remove_proxmox_cluster,
|
||||
|
||||
@ -618,3 +618,344 @@ export async function listMetricCollections(
|
||||
): Promise<any[]> {
|
||||
return await invoke<any[]>("list_metric_collections", { clusterId });
|
||||
}
|
||||
|
||||
// ─── HA (High Availability) ───────────────────────────────────────────────────
|
||||
|
||||
export interface HaGroup {
|
||||
id: string;
|
||||
nodes: string;
|
||||
comment?: string;
|
||||
restricted?: boolean;
|
||||
noQuorumPolicy?: string;
|
||||
}
|
||||
|
||||
export interface HaResource {
|
||||
sid: string;
|
||||
group?: string;
|
||||
state: string;
|
||||
maxRestart?: number;
|
||||
maxRelocate?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* List HA groups
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listHaGroups = async (clusterId: string): Promise<HaGroup[]> =>
|
||||
invoke<HaGroup[]>("list_ha_groups", { clusterId });
|
||||
|
||||
/**
|
||||
* Create an HA group
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param config - HA group configuration
|
||||
*/
|
||||
export const createHaGroup = async (
|
||||
clusterId: string,
|
||||
config: Partial<HaGroup>
|
||||
): Promise<void> => invoke<void>("create_ha_group", { clusterId, config });
|
||||
|
||||
/**
|
||||
* Update an HA group
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param id - HA group identifier
|
||||
* @param config - HA group configuration
|
||||
*/
|
||||
export const updateHaGroup = async (
|
||||
clusterId: string,
|
||||
id: string,
|
||||
config: Partial<HaGroup>
|
||||
): Promise<void> => invoke<void>("update_ha_group", { clusterId, id, config });
|
||||
|
||||
/**
|
||||
* Delete an HA group
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param id - HA group identifier
|
||||
*/
|
||||
export const deleteHaGroup = async (
|
||||
clusterId: string,
|
||||
id: string
|
||||
): Promise<void> => invoke<void>("delete_ha_group", { clusterId, id });
|
||||
|
||||
/**
|
||||
* List HA resources
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listHaResources = async (
|
||||
clusterId: string
|
||||
): Promise<HaResource[]> =>
|
||||
invoke<HaResource[]>("list_ha_resources", { clusterId });
|
||||
|
||||
/**
|
||||
* Enable an HA resource
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param id - HA resource identifier
|
||||
*/
|
||||
export const enableHaResource = async (
|
||||
clusterId: string,
|
||||
id: string
|
||||
): Promise<void> => invoke<void>("enable_ha_resource", { clusterId, id });
|
||||
|
||||
// ─── ACL / User Management ────────────────────────────────────────────────────
|
||||
|
||||
export interface AclEntry {
|
||||
path: string;
|
||||
type: "user" | "group" | "token";
|
||||
ugid: string;
|
||||
roleid: string;
|
||||
propagate?: boolean;
|
||||
}
|
||||
|
||||
export interface ProxmoxUser {
|
||||
userid: string;
|
||||
comment?: string;
|
||||
email?: string;
|
||||
enabled: boolean;
|
||||
expire?: number;
|
||||
firstname?: string;
|
||||
lastname?: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
export interface AuthRealm {
|
||||
realm: string;
|
||||
type: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List ACL entries
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listAcls = async (clusterId: string): Promise<AclEntry[]> =>
|
||||
invoke<AclEntry[]>("list_acls", { clusterId });
|
||||
|
||||
/**
|
||||
* List users
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listUsers = async (clusterId: string): Promise<ProxmoxUser[]> =>
|
||||
invoke<ProxmoxUser[]>("list_users", { clusterId });
|
||||
|
||||
/**
|
||||
* List authentication realms (typed)
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listRealms = async (clusterId: string): Promise<AuthRealm[]> =>
|
||||
invoke<AuthRealm[]>("list_realms", { clusterId });
|
||||
|
||||
// ─── Cluster Notes ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get cluster notes
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const getClusterNotes = async (clusterId: string): Promise<string> =>
|
||||
invoke<string>("get_cluster_notes", { clusterId });
|
||||
|
||||
/**
|
||||
* Update cluster notes
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param notes - Notes content
|
||||
*/
|
||||
export const updateClusterNotes = async (
|
||||
clusterId: string,
|
||||
notes: string
|
||||
): Promise<void> => invoke<void>("update_cluster_notes", { clusterId, notes });
|
||||
|
||||
// ─── Resource Search ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
type: "vm" | "container" | "node" | "storage" | "pool";
|
||||
name: string;
|
||||
node?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Proxmox resources
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param query - Search query string
|
||||
*/
|
||||
export const searchResources = async (
|
||||
clusterId: string,
|
||||
query: string
|
||||
): Promise<SearchResult[]> =>
|
||||
invoke<SearchResult[]>("search_proxmox_resources", { clusterId, query });
|
||||
|
||||
// ─── Node Status ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface NodeStatus {
|
||||
uptime: number;
|
||||
memory: { used: number; total: number };
|
||||
cpu: number;
|
||||
swap: { used: number; total: number };
|
||||
disk: { used: number; total: number };
|
||||
loadAvg: number[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node status
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param nodeId - Node identifier
|
||||
*/
|
||||
export const getNodeStatus = async (
|
||||
clusterId: string,
|
||||
nodeId: string
|
||||
): Promise<NodeStatus> =>
|
||||
invoke<NodeStatus>("get_node_status", { clusterId, nodeId });
|
||||
|
||||
// ─── APT (typed) ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AptPackage {
|
||||
package: string;
|
||||
version: string;
|
||||
newVersion?: string;
|
||||
priority: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AptRepository {
|
||||
types: string[];
|
||||
uris: string[];
|
||||
suites: string[];
|
||||
components: string[];
|
||||
enabled: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
// ─── Syslog ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SyslogEntry {
|
||||
n: number;
|
||||
t: string;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node syslog
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param nodeId - Node identifier
|
||||
* @param limit - Maximum number of entries (default 500)
|
||||
*/
|
||||
export const getSyslog = async (
|
||||
clusterId: string,
|
||||
nodeId: string,
|
||||
limit?: number
|
||||
): Promise<SyslogEntry[]> =>
|
||||
invoke<SyslogEntry[]>("get_syslog", {
|
||||
clusterId,
|
||||
nodeId,
|
||||
limit: limit ?? 500,
|
||||
});
|
||||
|
||||
// ─── Network Interfaces ───────────────────────────────────────────────────────
|
||||
|
||||
export interface NetworkInterface {
|
||||
iface: string;
|
||||
type: string;
|
||||
address?: string;
|
||||
netmask?: string;
|
||||
gateway?: string;
|
||||
active: boolean;
|
||||
autostart: boolean;
|
||||
comments?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List network interfaces on a node
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param nodeId - Node identifier
|
||||
*/
|
||||
export const listNetworkInterfaces = async (
|
||||
clusterId: string,
|
||||
nodeId: string
|
||||
): Promise<NetworkInterface[]> =>
|
||||
invoke<NetworkInterface[]>("list_network_interfaces", { clusterId, nodeId });
|
||||
|
||||
// ─── Cluster Views (typed) ────────────────────────────────────────────────────
|
||||
|
||||
export interface ClusterView {
|
||||
id: string;
|
||||
name: string;
|
||||
includes?: string[];
|
||||
excludes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* List cluster views
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const listClusterViews = async (
|
||||
clusterId: string
|
||||
): Promise<ClusterView[]> =>
|
||||
invoke<ClusterView[]>("list_cluster_views", { clusterId });
|
||||
|
||||
/**
|
||||
* Create a cluster view
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param config - View configuration
|
||||
*/
|
||||
export const createClusterView = async (
|
||||
clusterId: string,
|
||||
config: Partial<ClusterView>
|
||||
): Promise<void> =>
|
||||
invoke<void>("create_cluster_view", { clusterId, config });
|
||||
|
||||
/**
|
||||
* Delete a cluster view
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param viewId - View identifier
|
||||
*/
|
||||
export const deleteClusterView = async (
|
||||
clusterId: string,
|
||||
viewId: string
|
||||
): Promise<void> => invoke<void>("delete_cluster_view", { clusterId, viewId });
|
||||
|
||||
// ─── Subscription ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SubscriptionStatus {
|
||||
status: "active" | "expired" | "none";
|
||||
productname?: string;
|
||||
regdate?: string;
|
||||
nextduedate?: string;
|
||||
key?: string;
|
||||
serverid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription status
|
||||
* @param clusterId - Cluster identifier
|
||||
*/
|
||||
export const getSubscriptionStatus = async (
|
||||
clusterId: string
|
||||
): Promise<SubscriptionStatus> =>
|
||||
invoke<SubscriptionStatus>("get_subscription_status", { clusterId });
|
||||
|
||||
// ─── Cluster Task Log ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface ClusterTask {
|
||||
upid: string;
|
||||
node: string;
|
||||
pid: number;
|
||||
starttime: number;
|
||||
type: string;
|
||||
user: string;
|
||||
status?: string;
|
||||
exitstatus?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List cluster-level tasks
|
||||
* @param clusterId - Cluster identifier
|
||||
* @param limit - Maximum number of tasks to return (default 50)
|
||||
*/
|
||||
export const listClusterTasks = async (
|
||||
clusterId: string,
|
||||
limit?: number
|
||||
): Promise<ClusterTask[]> =>
|
||||
invoke<ClusterTask[]>("list_cluster_tasks", {
|
||||
clusterId,
|
||||
limit: limit ?? 50,
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user