Initial commit
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
use crate::error::Error;
|
||||
use crate::proxmox::{
|
||||
AddDiskConfig, AddNICConfig, ApiResponse, Backup, BackupJob, BackupJobConfig, ClusterStatus,
|
||||
CreateSnapshotConfig, Disk, EditNICConfig, NetworkInterface, Node, RestoreConfig, Snapshot,
|
||||
Storage, StorageContent, StorageDetail, Task, VM,
|
||||
};
|
||||
use crate::{CertificateInfo, ConnectionConfig, TermProxyResponse, VNCProxyResponse};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
struct Connection {
|
||||
config: ConnectionConfig,
|
||||
client: Client,
|
||||
current_endpoint_index: usize,
|
||||
}
|
||||
|
||||
pub struct ConnectionManager {
|
||||
connections: HashMap<String, Connection>,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connections: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_connection(&self, config: ConnectionConfig) -> crate::Result<()> {
|
||||
// In a real implementation, we'd store this to disk
|
||||
// For now, just validate the config
|
||||
if config.primary.url.is_empty() {
|
||||
return Err(Error::InvalidUrl("URL cannot be empty".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_connection(&self, id: &str) -> crate::Result<()> {
|
||||
// Remove from storage
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(&self, id: &str) -> crate::Result<()> {
|
||||
// Connect to the server
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self, id: &str) -> crate::Result<()> {
|
||||
// Disconnect from the server
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_certificate_info(&self, url: &str) -> crate::Result<CertificateInfo> {
|
||||
// Fetch certificate info from the server
|
||||
Ok(CertificateInfo {
|
||||
fingerprint: "AB:CD:EF:12:34:56:78:90".to_string(),
|
||||
issuer: "Proxmox".to_string(),
|
||||
subject: "pve".to_string(),
|
||||
valid_from: "2024-01-01".to_string(),
|
||||
valid_to: "2034-01-01".to_string(),
|
||||
self_signed: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn trust_certificate(&self, id: &str, fingerprint: &str) -> crate::Result<()> {
|
||||
// Store trusted certificate
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_nodes(&self, connection_id: &str) -> crate::Result<Vec<Node>> {
|
||||
// Fetch nodes from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_vms(&self, connection_id: &str) -> crate::Result<Vec<VM>> {
|
||||
// Fetch VMs from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_storage(&self, connection_id: &str) -> crate::Result<Vec<Storage>> {
|
||||
// Fetch storage from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_storage_content(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_storage: &str,
|
||||
) -> crate::Result<Vec<StorageContent>> {
|
||||
// Fetch content of a storage pool via Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_storage_detail(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_storage: &str,
|
||||
) -> crate::Result<StorageDetail> {
|
||||
// Fetch detailed info about a storage pool via Proxmox API
|
||||
Ok(StorageDetail {
|
||||
storage: String::new(),
|
||||
r#type: String::new(),
|
||||
content: String::new(),
|
||||
active: 0,
|
||||
enabled: 0,
|
||||
shared: 0,
|
||||
used: 0,
|
||||
total: 0,
|
||||
avail: 0,
|
||||
node: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_tasks(&self, connection_id: &str) -> crate::Result<Vec<Task>> {
|
||||
// Fetch tasks from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_cluster_status(&self, connection_id: &str) -> crate::Result<ClusterStatus> {
|
||||
// Fetch cluster status from Proxmox API
|
||||
Ok(ClusterStatus {
|
||||
r#type: "cluster".to_string(),
|
||||
name: "default".to_string(),
|
||||
id: "cluster/default".to_string(),
|
||||
nodes: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Start VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Stop VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Shutdown VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reboot_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Reboot VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn suspend_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Suspend (pause) VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resume_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> {
|
||||
// Resume suspended VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_disks(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
) -> crate::Result<Vec<Disk>> {
|
||||
// Fetch disks for a VM via Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn add_disk(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_config: AddDiskConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Add a disk to a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resize_disk(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_disk: &str,
|
||||
_size: u64,
|
||||
) -> crate::Result<()> {
|
||||
// Resize a disk via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_disk(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_disk: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Remove a disk via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn move_disk(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_disk: &str,
|
||||
_storage: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Move a disk to different storage via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_network_interfaces(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
) -> crate::Result<Vec<NetworkInterface>> {
|
||||
// Fetch network interfaces for a VM via Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn add_nic(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_config: AddNICConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Add a network interface to a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_nic(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_nic: &str,
|
||||
_config: EditNICConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Edit a network interface on a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_nic(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
_nic: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Remove a network interface from a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_snapshots(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
) -> crate::Result<Vec<Snapshot>> {
|
||||
// Fetch snapshots for a VM via Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn create_snapshot(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
_config: CreateSnapshotConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Create a snapshot for a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_snapshot(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
_name: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Delete a snapshot from a VM via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rollback_snapshot(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
_name: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Rollback a VM to a snapshot via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn migrate_vm(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
_target_node: &str,
|
||||
_online: bool,
|
||||
) -> crate::Result<()> {
|
||||
// Migrate a VM to another node via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_vnc_proxy(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
) -> crate::Result<VNCProxyResponse> {
|
||||
// Create a VNC proxy via Proxmox API
|
||||
// POST /nodes/{node}/qemu/{vmid}/vncproxy
|
||||
// Returns ticket, port, and certificate
|
||||
Ok(VNCProxyResponse {
|
||||
ticket: String::new(),
|
||||
port: 0,
|
||||
cert: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_term_proxy(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
_vmid: u32,
|
||||
) -> crate::Result<TermProxyResponse> {
|
||||
// Create a terminal proxy via Proxmox API
|
||||
// POST /nodes/{node}/lxc/{vmid}/termproxy
|
||||
// Returns ticket and port
|
||||
Ok(TermProxyResponse {
|
||||
ticket: String::new(),
|
||||
port: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_websocket_url(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_node: &str,
|
||||
) -> crate::Result<String> {
|
||||
// Build the WebSocket base URL from the connection config
|
||||
// Returns wss://{host}:{port} for the given connection
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
pub async fn get_backup_jobs(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
) -> crate::Result<Vec<BackupJob>> {
|
||||
// Fetch backup jobs from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn get_backups(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_storage: Option<&str>,
|
||||
) -> crate::Result<Vec<Backup>> {
|
||||
// Fetch existing backups from Proxmox API
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn create_backup_job(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_config: BackupJobConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Create a backup job via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_backup_job(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_id: &str,
|
||||
_config: BackupJobConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Update a backup job via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_backup_job(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_id: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Delete a backup job via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_backup(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_config: BackupJobConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Trigger an immediate backup run via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restore_backup(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_volid: &str,
|
||||
_config: RestoreConfig,
|
||||
) -> crate::Result<()> {
|
||||
// Restore a backup via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_backup(
|
||||
&self,
|
||||
_connection_id: &str,
|
||||
_volid: &str,
|
||||
) -> crate::Result<()> {
|
||||
// Delete a backup file via Proxmox API
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Connection not found: {0}")]
|
||||
ConnectionNotFound(String),
|
||||
|
||||
#[error("Not connected to server")]
|
||||
NotConnected,
|
||||
|
||||
#[error("HTTP request failed: {0}")]
|
||||
HttpError(#[from] reqwest::Error),
|
||||
|
||||
#[error("Certificate error: {0}")]
|
||||
CertificateError(String),
|
||||
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthError(String),
|
||||
|
||||
#[error("Keyring error: {0}")]
|
||||
KeyringError(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(String),
|
||||
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
#[error("API error: {0}")]
|
||||
ApiError(String),
|
||||
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocketError(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::Manager;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
mod connection;
|
||||
mod proxmox;
|
||||
mod error;
|
||||
mod websocket;
|
||||
|
||||
use connection::ConnectionManager;
|
||||
use error::Error;
|
||||
use websocket::WebSocketManager;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub primary: EndpointConfig,
|
||||
pub fallbacks: Vec<EndpointConfig>,
|
||||
pub cert_fingerprint: Option<String>,
|
||||
pub trusted: bool,
|
||||
pub status: String,
|
||||
pub cluster_name: Option<String>,
|
||||
pub is_cluster: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct EndpointConfig {
|
||||
pub url: String,
|
||||
pub node: Option<String>,
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct CertificateInfo {
|
||||
pub fingerprint: String,
|
||||
pub issuer: String,
|
||||
pub subject: String,
|
||||
pub valid_from: String,
|
||||
pub valid_to: String,
|
||||
pub self_signed: bool,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
connection_manager: Arc<RwLock<ConnectionManager>>,
|
||||
ws_manager: Arc<RwLock<WebSocketManager>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_connection(
|
||||
state: tauri::State<'_, AppState>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.add_connection(config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_connection(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.remove_connection(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn connect_to_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.connect(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn disconnect_from_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.disconnect(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_certificate_info(
|
||||
state: tauri::State<'_, AppState>,
|
||||
url: String,
|
||||
) -> Result<CertificateInfo> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_certificate_info(&url).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn trust_certificate(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
fingerprint: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.trust_certificate(&id, &fingerprint).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_nodes(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<proxmox::Node>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_nodes(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_vms(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<proxmox::VM>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_vms(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_storage(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<proxmox::Storage>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_storage(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_storage_content(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
storage: String,
|
||||
) -> Result<Vec<proxmox::StorageContent>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_storage_content(&connection_id, &storage).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_storage_detail(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
storage: String,
|
||||
) -> Result<proxmox::StorageDetail> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_storage_detail(&connection_id, &node, &storage).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_tasks(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<proxmox::Task>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_tasks(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_cluster_status(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<proxmox::ClusterStatus> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_cluster_status(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.start_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.stop_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn shutdown_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.shutdown_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn reboot_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.reboot_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn suspend_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.suspend_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn resume_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.resume_vm(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_disks(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<Vec<proxmox::Disk>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_disks(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_disk(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
config: proxmox::AddDiskConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.add_disk(&connection_id, &node, vmid, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn resize_disk(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
disk: String,
|
||||
size: u64,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.resize_disk(&connection_id, &node, vmid, &disk, size).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_disk(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
disk: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.remove_disk(&connection_id, &node, vmid, &disk).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn move_disk(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
disk: String,
|
||||
storage: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.move_disk(&connection_id, &node, vmid, &disk, &storage).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_network_interfaces(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<Vec<proxmox::NetworkInterface>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_network_interfaces(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_nic(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
config: proxmox::AddNICConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.add_nic(&connection_id, &node, vmid, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn edit_nic(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
nic: String,
|
||||
config: proxmox::EditNICConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.edit_nic(&connection_id, &node, vmid, &nic, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_nic(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
nic: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.remove_nic(&connection_id, &node, vmid, &nic).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_snapshots(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<Vec<proxmox::Snapshot>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_snapshots(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_snapshot(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
config: proxmox::CreateSnapshotConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.create_snapshot(&connection_id, &node, vmid, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_snapshot(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
name: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.delete_snapshot(&connection_id, &node, vmid, &name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn rollback_snapshot(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
name: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.rollback_snapshot(&connection_id, &node, vmid, &name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn migrate_vm(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
target_node: String,
|
||||
online: bool,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.migrate_vm(&connection_id, &node, vmid, &target_node, online).await
|
||||
}
|
||||
|
||||
// Console proxy types
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct VNCProxyResponse {
|
||||
pub ticket: String,
|
||||
pub port: u32,
|
||||
pub cert: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TermProxyResponse {
|
||||
pub ticket: String,
|
||||
pub port: u32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_vnc_proxy(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<VNCProxyResponse> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.create_vnc_proxy(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_term_proxy(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
vmid: u32,
|
||||
) -> Result<TermProxyResponse> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.create_term_proxy(&connection_id, &node, vmid).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_websocket_url(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
node: String,
|
||||
) -> Result<String> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_websocket_url(&connection_id, &node).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn connect_websocket(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
url: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<()> {
|
||||
let mut ws_manager = state.ws_manager.write().await;
|
||||
ws_manager.connect(connection_id, url, app_handle).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn disconnect_websocket(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<()> {
|
||||
let mut ws_manager = state.ws_manager.write().await;
|
||||
ws_manager.disconnect(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn is_websocket_connected(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<bool> {
|
||||
let ws_manager = state.ws_manager.read().await;
|
||||
Ok(ws_manager.is_connected(&connection_id))
|
||||
}
|
||||
|
||||
// Backup management commands
|
||||
#[tauri::command]
|
||||
async fn get_backup_jobs(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<proxmox::BackupJob>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_backup_jobs(&connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_backups(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
storage: Option<String>,
|
||||
) -> Result<Vec<proxmox::Backup>> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.get_backups(&connection_id, storage.as_deref()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_backup_job(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
config: proxmox::BackupJobConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.create_backup_job(&connection_id, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_backup_job(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
id: String,
|
||||
config: proxmox::BackupJobConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.update_backup_job(&connection_id, &id, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_backup_job(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.delete_backup_job(&connection_id, &id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn run_backup(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
config: proxmox::BackupJobConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.run_backup(&connection_id, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn restore_backup(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
volid: String,
|
||||
config: proxmox::RestoreConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.restore_backup(&connection_id, &volid, config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_backup(
|
||||
state: tauri::State<'_, AppState>,
|
||||
connection_id: String,
|
||||
volid: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
manager.delete_backup(&connection_id, &volid).await
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TrayConnectionInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_tray_menu(
|
||||
app: tauri::AppHandle,
|
||||
connections: Vec<TrayConnectionInfo>,
|
||||
) -> Result<()> {
|
||||
let mut menu_builder = MenuBuilder::new(&app);
|
||||
|
||||
// Show/Hide window item
|
||||
let show_hide = MenuItemBuilder::new("Show / Hide")
|
||||
.id("show_hide")
|
||||
.build(&app)?;
|
||||
menu_builder = menu_builder.item(&show_hide);
|
||||
menu_builder = menu_builder.separator();
|
||||
|
||||
// Connection items with status
|
||||
for conn in &connections {
|
||||
let status_icon = match conn.status.as_str() {
|
||||
"connected" => "🟢",
|
||||
"connecting" | "failover" => "🟡",
|
||||
"failed" => "🔴",
|
||||
_ => "⚪",
|
||||
};
|
||||
let label = format!("{} {}", status_icon, conn.name);
|
||||
let item = MenuItemBuilder::new(&label)
|
||||
.id(format!("connection_{}", conn.id))
|
||||
.build(&app)?;
|
||||
menu_builder = menu_builder.item(&item);
|
||||
}
|
||||
|
||||
if connections.is_empty() {
|
||||
let no_conn = MenuItemBuilder::new("No connections")
|
||||
.id("no_connections")
|
||||
.disabled(true)
|
||||
.build(&app)?;
|
||||
menu_builder = menu_builder.item(&no_conn);
|
||||
}
|
||||
|
||||
menu_builder = menu_builder.separator();
|
||||
|
||||
// Quit item
|
||||
let quit = MenuItemBuilder::new("Quit").id("quit").build(&app)?;
|
||||
menu_builder = menu_builder.item(&quit);
|
||||
|
||||
let menu = menu_builder.build()?;
|
||||
|
||||
// Update the tray menu
|
||||
if let Some(tray) = app.tray_by_id("main-tray") {
|
||||
tray.set_menu(Some(menu))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(AppState {
|
||||
connection_manager: Arc::new(RwLock::new(ConnectionManager::new())),
|
||||
ws_manager: Arc::new(RwLock::new(WebSocketManager::new())),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
add_connection,
|
||||
remove_connection,
|
||||
connect_to_server,
|
||||
disconnect_from_server,
|
||||
get_certificate_info,
|
||||
trust_certificate,
|
||||
get_nodes,
|
||||
get_vms,
|
||||
get_storage,
|
||||
get_storage_content,
|
||||
get_storage_detail,
|
||||
get_tasks,
|
||||
get_cluster_status,
|
||||
start_vm,
|
||||
stop_vm,
|
||||
shutdown_vm,
|
||||
reboot_vm,
|
||||
suspend_vm,
|
||||
resume_vm,
|
||||
get_disks,
|
||||
add_disk,
|
||||
resize_disk,
|
||||
remove_disk,
|
||||
move_disk,
|
||||
get_network_interfaces,
|
||||
add_nic,
|
||||
edit_nic,
|
||||
remove_nic,
|
||||
get_snapshots,
|
||||
create_snapshot,
|
||||
delete_snapshot,
|
||||
rollback_snapshot,
|
||||
migrate_vm,
|
||||
create_vnc_proxy,
|
||||
create_term_proxy,
|
||||
get_websocket_url,
|
||||
connect_websocket,
|
||||
disconnect_websocket,
|
||||
is_websocket_connected,
|
||||
get_backup_jobs,
|
||||
get_backups,
|
||||
create_backup_job,
|
||||
update_backup_job,
|
||||
delete_backup_job,
|
||||
run_backup,
|
||||
restore_backup,
|
||||
delete_backup,
|
||||
update_tray_menu,
|
||||
])
|
||||
.setup(|app| {
|
||||
// Build the system tray menu
|
||||
let show_hide = MenuItemBuilder::new("Show / Hide")
|
||||
.id("show_hide")
|
||||
.build(app)?;
|
||||
let quit = MenuItemBuilder::new("Quit").id("quit").build(app)?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&show_hide)
|
||||
.separator()
|
||||
.item(&quit)
|
||||
.build()?;
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.id("main-tray")
|
||||
.tooltip("ProxmoxDesktop")
|
||||
.icon(app.default_window_icon().cloned().expect("no default icon"))
|
||||
.menu(&menu)
|
||||
.on_menu_event(move |app, event| {
|
||||
match event.id.as_ref() {
|
||||
"show_hide" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
proxmox_desktop::run()
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Disk {
|
||||
pub device: String,
|
||||
pub size: u64,
|
||||
pub storage: String,
|
||||
pub format: String,
|
||||
pub usage: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddDiskConfig {
|
||||
pub storage: String,
|
||||
pub size: u64,
|
||||
pub bus_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub node: String,
|
||||
pub status: String,
|
||||
pub cpu: f64,
|
||||
pub maxcpu: u32,
|
||||
pub mem: u64,
|
||||
pub maxmem: u64,
|
||||
pub disk: u64,
|
||||
pub maxdisk: u64,
|
||||
pub uptime: u64,
|
||||
pub level: String,
|
||||
pub id: String,
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VM {
|
||||
pub vmid: u32,
|
||||
pub name: Option<String>,
|
||||
pub status: String,
|
||||
pub r#type: String,
|
||||
pub node: String,
|
||||
pub cpu: f64,
|
||||
pub cpus: u32,
|
||||
pub mem: u64,
|
||||
pub maxmem: u64,
|
||||
pub disk: u64,
|
||||
pub maxdisk: u64,
|
||||
pub uptime: u64,
|
||||
pub netin: u64,
|
||||
pub netout: u64,
|
||||
pub diskread: u64,
|
||||
pub diskwrite: u64,
|
||||
pub pid: Option<u32>,
|
||||
pub template: Option<u32>,
|
||||
pub lock: Option<String>,
|
||||
pub tags: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Storage {
|
||||
pub storage: String,
|
||||
pub r#type: String,
|
||||
pub content: String,
|
||||
pub active: u32,
|
||||
pub enabled: u32,
|
||||
pub shared: u32,
|
||||
pub used: u64,
|
||||
pub total: u64,
|
||||
pub avail: u64,
|
||||
pub node: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
pub upid: String,
|
||||
pub node: String,
|
||||
pub pid: u32,
|
||||
pub pstart: u64,
|
||||
pub starttime: u64,
|
||||
pub endtime: Option<u64>,
|
||||
pub r#type: String,
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub status: Option<String>,
|
||||
pub exitstatus: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterStatus {
|
||||
pub r#type: String,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub nodes: Option<Vec<ClusterNode>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterNode {
|
||||
pub name: String,
|
||||
pub nodeid: u32,
|
||||
pub online: u32,
|
||||
pub local: Option<u32>,
|
||||
pub ip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Snapshot {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub snaptime: u64,
|
||||
pub vmstate: u32,
|
||||
pub parent: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateSnapshotConfig {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub vmstate: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkInterface {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub macaddr: String,
|
||||
pub bridge: Option<String>,
|
||||
pub tag: Option<u32>,
|
||||
pub firewall: Option<u32>,
|
||||
pub link_down: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddNICConfig {
|
||||
pub bridge: String,
|
||||
pub model: String,
|
||||
pub macaddr: Option<String>,
|
||||
pub tag: Option<u32>,
|
||||
pub firewall: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EditNICConfig {
|
||||
pub bridge: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub tag: Option<u32>,
|
||||
pub firewall: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Backup {
|
||||
pub volid: String,
|
||||
pub backupid: String,
|
||||
#[serde(rename = "backup-type")]
|
||||
pub backup_type: String,
|
||||
#[serde(rename = "backup-id")]
|
||||
pub backup_id: String,
|
||||
#[serde(rename = "backup-time")]
|
||||
pub backup_time: u64,
|
||||
pub storage: String,
|
||||
pub size: u64,
|
||||
pub ctime: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupJob {
|
||||
pub id: String,
|
||||
pub store: String,
|
||||
pub schedule: String,
|
||||
pub all: u32,
|
||||
pub enabled: u32,
|
||||
pub node: Option<String>,
|
||||
pub vmid: Option<String>,
|
||||
pub compress: Option<String>,
|
||||
pub mode: Option<String>,
|
||||
pub quiet: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupJobConfig {
|
||||
pub id: Option<String>,
|
||||
pub storage: String,
|
||||
pub schedule: String,
|
||||
pub mode: String,
|
||||
pub compression: String,
|
||||
pub all: bool,
|
||||
pub vmid: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub node: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RestoreConfig {
|
||||
pub volid: String,
|
||||
pub node: String,
|
||||
pub storage: String,
|
||||
pub vmid: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageContent {
|
||||
pub content: String,
|
||||
pub ctime: u64,
|
||||
pub format: Option<String>,
|
||||
pub size: Option<u64>,
|
||||
pub subtype: Option<String>,
|
||||
pub volid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageDetail {
|
||||
pub storage: String,
|
||||
pub r#type: String,
|
||||
pub content: String,
|
||||
pub active: u32,
|
||||
pub enabled: u32,
|
||||
pub shared: u32,
|
||||
pub used: u64,
|
||||
pub total: u64,
|
||||
pub avail: u64,
|
||||
pub node: String,
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::connect_async;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskUpdate {
|
||||
pub connection_id: String,
|
||||
pub upid: String,
|
||||
pub node: String,
|
||||
pub task_type: String,
|
||||
pub status: Option<String>,
|
||||
pub exitstatus: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeStatusChange {
|
||||
pub connection_id: String,
|
||||
pub node: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VMStatusChange {
|
||||
pub connection_id: String,
|
||||
pub node: String,
|
||||
pub vmid: u32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Manages WebSocket connections per connection ID.
|
||||
///
|
||||
/// Each connection ID maps to a background task that reads messages from
|
||||
/// the Proxmox WebSocket and re-emits them as Tauri events.
|
||||
pub struct WebSocketManager {
|
||||
connections: HashMap<String, mpsc::Sender<()>>,
|
||||
}
|
||||
|
||||
impl WebSocketManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connections: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to a Proxmox WebSocket URL for the given connection ID.
|
||||
///
|
||||
/// Messages are forwarded as Tauri events via `app_handle`. If a connection
|
||||
/// already exists for this ID, it is disconnected first.
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
connection_id: String,
|
||||
url: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> crate::Result<()> {
|
||||
// Disconnect any existing connection for this ID
|
||||
self.disconnect(&connection_id).await;
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let cid = connection_id.clone();
|
||||
let ws_url = url.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut reconnect_delay = Duration::from_secs(1);
|
||||
const MAX_DELAY: Duration = Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
result = connect_and_run(&cid, &ws_url, &app_handle) => {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
// Normal close or stream ended – attempt reconnect
|
||||
reconnect_delay = Duration::from_secs(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ws] connection error for {}: {e}", cid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect back-off
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = sleep(reconnect_delay) => {}
|
||||
}
|
||||
reconnect_delay = (reconnect_delay * 2).min(MAX_DELAY);
|
||||
}
|
||||
});
|
||||
|
||||
self.connections.insert(connection_id, shutdown_tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect the WebSocket for the given connection ID.
|
||||
pub async fn disconnect(&mut self, connection_id: &str) -> crate::Result<()> {
|
||||
if let Some(tx) = self.connections.remove(connection_id) {
|
||||
let _ = tx.send(()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether a WebSocket connection is active.
|
||||
pub fn is_connected(&self, connection_id: &str) -> bool {
|
||||
self.connections.contains_key(connection_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the Proxmox WebSocket and relay messages as Tauri events.
|
||||
async fn connect_and_run(
|
||||
connection_id: &str,
|
||||
url: &str,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> crate::Result<()> {
|
||||
let (ws_stream, _) = connect_async(url)
|
||||
.await
|
||||
.map_err(|e| Error::WebSocketError(e.to_string()))?;
|
||||
|
||||
let cid = connection_id.to_string();
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
while let Some(msg_result) = read.next().await {
|
||||
match msg_result {
|
||||
Ok(Message::Text(text)) => {
|
||||
handle_ws_message(&cid, &text, app_handle);
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("[ws] read error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt clean close
|
||||
let _ = write.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a Proxmox WebSocket message and emit the appropriate Tauri event.
|
||||
///
|
||||
/// Proxmox sends JSON messages in the format:
|
||||
/// ```json
|
||||
/// { "type": "task", "data": { ... } }
|
||||
/// ```
|
||||
/// or various status update formats. We try to detect known types and emit
|
||||
/// events, falling back to a generic broadcast.
|
||||
fn handle_ws_message(connection_id: &str, text: &str, app_handle: &tauri::AppHandle) {
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to detect task-related messages
|
||||
if let Some(msg_type) = value.get("type").and_then(|v| v.as_str()) {
|
||||
match msg_type {
|
||||
"task" => {
|
||||
if let Some(data) = value.get("data") {
|
||||
let update = TaskUpdate {
|
||||
connection_id: connection_id.to_string(),
|
||||
upid: data
|
||||
.get("upid")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
node: data
|
||||
.get("node")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
task_type: data
|
||||
.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
status: data
|
||||
.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
exitstatus: data
|
||||
.get("exitstatus")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
};
|
||||
let _ = app_handle.emit("task-update", update);
|
||||
}
|
||||
}
|
||||
"node" => {
|
||||
if let Some(data) = value.get("data") {
|
||||
let change = NodeStatusChange {
|
||||
connection_id: connection_id.to_string(),
|
||||
node: data
|
||||
.get("node")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
status: data
|
||||
.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
};
|
||||
let _ = app_handle.emit("node-status-change", change);
|
||||
}
|
||||
}
|
||||
"vm" => {
|
||||
if let Some(data) = value.get("data") {
|
||||
let change = VMStatusChange {
|
||||
connection_id: connection_id.to_string(),
|
||||
node: data
|
||||
.get("node")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
vmid: data
|
||||
.get("vmid")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32,
|
||||
status: data
|
||||
.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
};
|
||||
let _ = app_handle.emit("vm-status-change", change);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown message type – emit generic event with raw data
|
||||
let _ = app_handle.emit(
|
||||
"ws-raw",
|
||||
serde_json::json!({
|
||||
"connection_id": connection_id,
|
||||
"data": value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user