feat: implement v1.2.1 fixes #95

Merged
sarman merged 11 commits from fix/proxmox-v1.2.1 into master 2026-06-13 03:50:35 +00:00
6 changed files with 1011 additions and 1 deletions
Showing only changes of commit 38eecaafcf - Show all commits

File diff suppressed because one or more lines are too long

View File

@ -6451,6 +6451,60 @@
"type": "string", "type": "string",
"const": "stronghold:deny-save-store-record", "const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope." "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."
} }
] ]
}, },

View File

@ -6451,6 +6451,60 @@
"type": "string", "type": "string",
"const": "stronghold:deny-save-store-record", "const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope." "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."
} }
] ]
}, },

View File

@ -1583,6 +1583,537 @@ pub async fn list_metric_collections(
Ok(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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -191,6 +191,36 @@ pub fn run() {
// Proxmox - Infrastructure (Phase 5) // Proxmox - Infrastructure (Phase 5)
commands::proxmox::get_metrics_summary, commands::proxmox::get_metrics_summary,
commands::proxmox::list_metric_collections, 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 // Proxmox - Existing
commands::proxmox::add_proxmox_cluster, commands::proxmox::add_proxmox_cluster,
commands::proxmox::remove_proxmox_cluster, commands::proxmox::remove_proxmox_cluster,

View File

@ -618,3 +618,344 @@ export async function listMetricCollections(
): Promise<any[]> { ): Promise<any[]> {
return await invoke<any[]>("list_metric_collections", { clusterId }); 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,
});