feature/proxmox-v1.2.0 #90
@ -437,6 +437,7 @@ pub async fn initiate_oauth(
|
||||
let watchers = app_state.watchers.clone();
|
||||
let log_streams = app_state.log_streams.clone();
|
||||
let pty_sessions = app_state.pty_sessions.clone();
|
||||
let proxmox_clusters = app_state.proxmox_clusters.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let app_state_for_callback = AppState {
|
||||
@ -452,6 +453,7 @@ pub async fn initiate_oauth(
|
||||
watchers,
|
||||
log_streams,
|
||||
pty_sessions,
|
||||
proxmox_clusters,
|
||||
};
|
||||
while let Some(callback) = callback_rx.recv().await {
|
||||
tracing::info!("Received OAuth callback for state: {}", callback.state);
|
||||
|
||||
@ -7,5 +7,6 @@ pub mod image;
|
||||
pub mod integrations;
|
||||
pub mod kube;
|
||||
pub mod metrics;
|
||||
pub mod proxmox;
|
||||
pub mod shell;
|
||||
pub mod system;
|
||||
|
||||
235
src-tauri/src/commands/proxmox.rs
Normal file
235
src-tauri/src/commands/proxmox.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use crate::proxmox::{ClusterInfo, ClusterType, ProxmoxClient};
|
||||
use crate::state::AppState;
|
||||
use chrono::Utc;
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Proxmox cluster connection information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterConnection {
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Add a Proxmox cluster
|
||||
#[tauri::command]
|
||||
pub async fn add_proxmox_cluster(
|
||||
id: String,
|
||||
name: String,
|
||||
cluster_type: ClusterType,
|
||||
connection: ClusterConnection,
|
||||
username: String,
|
||||
password: &str,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ClusterInfo, String> {
|
||||
// Create client and authenticate
|
||||
let client = ProxmoxClient::new(&connection.url, connection.port, &username);
|
||||
let ticket = client
|
||||
.authenticate(password)
|
||||
.await
|
||||
.map_err(|e| format!("Authentication failed: {}", e))?;
|
||||
|
||||
// Encrypt credentials for storage
|
||||
let credentials = serde_json::json!({
|
||||
"ticket": ticket,
|
||||
"username": username
|
||||
});
|
||||
let encrypted_credentials = crate::integrations::auth::encrypt_token(
|
||||
&serde_json::to_string(&credentials).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| format!("Failed to encrypt credentials: {}", e))?;
|
||||
|
||||
// Create cluster info
|
||||
let cluster = ClusterInfo {
|
||||
id: id.clone(),
|
||||
name,
|
||||
cluster_type,
|
||||
url: connection.url,
|
||||
port: connection.port,
|
||||
username,
|
||||
created_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
updated_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
};
|
||||
|
||||
// Store in database
|
||||
{
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock database: {}", e))?;
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO proxmox_clusters (id, name, cluster_type, url, port, auth_method, encrypted_credentials, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
rusqlite::params![
|
||||
cluster.id,
|
||||
cluster.name,
|
||||
match cluster.cluster_type {
|
||||
ClusterType::VE => "ve",
|
||||
ClusterType::PBS => "pbs",
|
||||
},
|
||||
cluster.url,
|
||||
cluster.port,
|
||||
"root",
|
||||
encrypted_credentials,
|
||||
cluster.created_at,
|
||||
cluster.updated_at,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to store cluster: {}", e))?;
|
||||
}
|
||||
|
||||
// Store in memory for quick access
|
||||
{
|
||||
let mut clusters = state.proxmox_clusters.lock().await;
|
||||
clusters.insert(id, Arc::new(Mutex::new(client)));
|
||||
}
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// Remove a Proxmox cluster
|
||||
#[tauri::command]
|
||||
pub async fn remove_proxmox_cluster(id: String, state: State<'_, AppState>) -> Result<(), String> {
|
||||
// Remove from database
|
||||
{
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock database: {}", e))?;
|
||||
|
||||
db.execute("DELETE FROM proxmox_clusters WHERE id = ?1", [id.clone()])
|
||||
.map_err(|e| format!("Failed to remove cluster: {}", e))?;
|
||||
}
|
||||
|
||||
// Remove from memory
|
||||
{
|
||||
let mut clusters = state.clusters.lock().await;
|
||||
clusters.remove(&id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all Proxmox clusters
|
||||
#[tauri::command]
|
||||
pub async fn list_proxmox_clusters(state: State<'_, AppState>) -> Result<Vec<ClusterInfo>, String> {
|
||||
let clusters = {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock database: {}", e))?;
|
||||
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters",
|
||||
)
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let cluster_iter = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(ClusterInfo {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
cluster_type: match row.get::<_, String>(2)?.as_str() {
|
||||
"ve" => ClusterType::VE,
|
||||
"pbs" => ClusterType::PBS,
|
||||
_ => ClusterType::VE,
|
||||
},
|
||||
url: row.get(3)?,
|
||||
port: row.get(4)?,
|
||||
username: "".to_string(), // Will be decrypted when needed
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to query clusters: {}", e))?;
|
||||
|
||||
cluster_iter
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
|
||||
clusters
|
||||
}
|
||||
|
||||
/// Get a specific Proxmox cluster
|
||||
#[tauri::command]
|
||||
pub async fn get_proxmox_cluster(
|
||||
id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<ClusterInfo>, String> {
|
||||
let cluster = {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock database: {}", e))?;
|
||||
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters WHERE id = ?1",
|
||||
)
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
stmt.query_row([id], |row| {
|
||||
Ok(ClusterInfo {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
cluster_type: match row.get::<_, String>(2)?.as_str() {
|
||||
"ve" => ClusterType::VE,
|
||||
"pbs" => ClusterType::PBS,
|
||||
_ => ClusterType::VE,
|
||||
},
|
||||
url: row.get(3)?,
|
||||
port: row.get(4)?,
|
||||
username: "".to_string(),
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
.map_err(|e| format!("Failed to query cluster: {}", e))?
|
||||
};
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cluster_type_serialization() {
|
||||
let json = serde_json::to_string(&ClusterType::VE).unwrap();
|
||||
assert_eq!(json, "\"ve\"");
|
||||
|
||||
let ve: ClusterType = serde_json::from_str("\"ve\"").unwrap();
|
||||
assert_eq!(ve, ClusterType::VE);
|
||||
|
||||
let pbs: ClusterType = serde_json::from_str("\"pbs\"").unwrap();
|
||||
assert_eq!(pbs, ClusterType::PBS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cluster_info_serialization() {
|
||||
let cluster = ClusterInfo {
|
||||
id: "proxmox-1".to_string(),
|
||||
name: "Production".to_string(),
|
||||
cluster_type: ClusterType::VE,
|
||||
url: "https://pve.example.com".to_string(),
|
||||
port: 8006,
|
||||
username: "root@pam".to_string(),
|
||||
created_at: "2026-06-10 12:00:00".to_string(),
|
||||
updated_at: "2026-06-10 12:00:00".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&cluster).unwrap();
|
||||
let deserialized: ClusterInfo = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(cluster.id, deserialized.id);
|
||||
assert_eq!(cluster.name, deserialized.name);
|
||||
}
|
||||
}
|
||||
@ -394,6 +394,38 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
|
||||
CREATE INDEX IF NOT EXISTS idx_port_forwards_status ON port_forwards(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_port_forwards_namespace ON port_forwards(namespace);",
|
||||
),
|
||||
(
|
||||
"031_create_proxmox_clusters",
|
||||
"CREATE TABLE IF NOT EXISTS proxmox_clusters (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
cluster_type TEXT NOT NULL CHECK(cluster_type IN ('ve', 'pbs')),
|
||||
url TEXT NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 8006,
|
||||
auth_method TEXT NOT NULL DEFAULT 'root',
|
||||
encrypted_credentials TEXT NOT NULL,
|
||||
ssl_fingerprint TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_clusters_name ON proxmox_clusters(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_clusters_type ON proxmox_clusters(cluster_type);",
|
||||
),
|
||||
(
|
||||
"032_create_proxmox_resources",
|
||||
"CREATE TABLE IF NOT EXISTS proxmox_resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
cluster_id TEXT NOT NULL REFERENCES proxmox_clusters(id) ON DELETE CASCADE,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
resource_data TEXT NOT NULL DEFAULT '{}',
|
||||
last_updated TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(cluster_id, resource_type, resource_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_resources_cluster ON proxmox_resources(cluster_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_resources_type ON proxmox_resources(resource_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_resources_updated ON proxmox_resources(last_updated);",
|
||||
),
|
||||
];
|
||||
|
||||
for (name, sql) in migrations {
|
||||
|
||||
@ -9,6 +9,7 @@ pub mod mcp;
|
||||
pub mod metrics;
|
||||
pub mod ollama;
|
||||
pub mod pii;
|
||||
pub mod proxmox;
|
||||
pub mod shell;
|
||||
pub mod state;
|
||||
|
||||
@ -43,6 +44,7 @@ pub fn run() {
|
||||
mcp_connections: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
pending_approvals: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
proxmox_clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
refresh_registry: Arc::new(tokio::sync::Mutex::new(crate::kube::RefreshRegistry::new())),
|
||||
watchers: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||
@ -147,6 +149,11 @@ pub fn run() {
|
||||
commands::integrations::save_integration_config,
|
||||
commands::integrations::get_integration_config,
|
||||
commands::integrations::get_all_integration_configs,
|
||||
// Proxmox
|
||||
commands::proxmox::add_proxmox_cluster,
|
||||
commands::proxmox::remove_proxmox_cluster,
|
||||
commands::proxmox::list_proxmox_clusters,
|
||||
commands::proxmox::get_proxmox_cluster,
|
||||
// System / Settings
|
||||
commands::system::check_ollama_installed,
|
||||
commands::system::get_ollama_install_guide,
|
||||
|
||||
98
src-tauri/src/proxmox/backup.rs
Normal file
98
src-tauri/src/proxmox/backup.rs
Normal file
@ -0,0 +1,98 @@
|
||||
// Backup management module
|
||||
// Provides operations for managing Proxmox Backup Server
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Backup job information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupJob {
|
||||
pub job_id: u32,
|
||||
pub name: String,
|
||||
pub schedule: String,
|
||||
pub enabled: bool,
|
||||
pub datastore: String,
|
||||
pub source: String,
|
||||
pub retention: String,
|
||||
}
|
||||
|
||||
/// Datastore information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatastoreInfo {
|
||||
pub datastore: String,
|
||||
pub node: String,
|
||||
pub size: u64,
|
||||
pub used: u64,
|
||||
pub available: u64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List backup jobs
|
||||
pub async fn list_backup_jobs(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<BackupJob>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Create backup job
|
||||
pub async fn create_backup_job(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_job: &BackupJob,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Delete backup job
|
||||
pub async fn delete_backup_job(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_job_id: u32,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Trigger backup job manually
|
||||
pub async fn trigger_backup_job(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_job_id: u32,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List datastores
|
||||
pub async fn list_datastores(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<DatastoreInfo>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_backup_job_serialization() {
|
||||
let job = BackupJob {
|
||||
job_id: 1,
|
||||
name: "daily-backup".to_string(),
|
||||
schedule: "0 2 * * *".to_string(),
|
||||
enabled: true,
|
||||
datastore: "pbs-datastore".to_string(),
|
||||
source: "/data".to_string(),
|
||||
retention: "30d".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&job).unwrap();
|
||||
let deserialized: BackupJob = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(job.name, deserialized.name);
|
||||
assert_eq!(job.enabled, deserialized.enabled);
|
||||
}
|
||||
}
|
||||
113
src-tauri/src/proxmox/ceph.rs
Normal file
113
src-tauri/src/proxmox/ceph.rs
Normal file
@ -0,0 +1,113 @@
|
||||
// Ceph management module
|
||||
// Provides operations for managing Ceph clusters
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Ceph pool information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CephPool {
|
||||
pub pool: String,
|
||||
pub pool_id: u64,
|
||||
pub size: u32,
|
||||
pub min_size: u32,
|
||||
pub pg_num: u32,
|
||||
pub used: u64,
|
||||
pub avail: u64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Ceph OSD information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CephOsd {
|
||||
pub osd: u32,
|
||||
pub up: bool,
|
||||
pub in_: bool,
|
||||
pub weight: f64,
|
||||
pub pg_num: u32,
|
||||
pub usage: f64,
|
||||
}
|
||||
|
||||
/// Ceph monitor information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CephMonitor {
|
||||
pub name: String,
|
||||
pub quorum: bool,
|
||||
pub address: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Ceph health status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CephHealth {
|
||||
pub status: String,
|
||||
pub summary: String,
|
||||
pub details: Vec<String>,
|
||||
}
|
||||
|
||||
/// List Ceph pools
|
||||
pub async fn list_pools(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<CephPool>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Create Ceph pool
|
||||
pub async fn create_pool(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_pool: &str,
|
||||
_pg_num: u32,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Delete Ceph pool
|
||||
pub async fn delete_pool(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_pool: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List Ceph OSDs
|
||||
pub async fn list_osds(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<CephOsd>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Get Ceph health
|
||||
pub async fn get_ceph_health(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<CephHealth, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ceph_pool_serialization() {
|
||||
let pool = CephPool {
|
||||
pool: "rbd".to_string(),
|
||||
pool_id: 1,
|
||||
size: 3,
|
||||
min_size: 2,
|
||||
pg_num: 128,
|
||||
used: 1000000000000,
|
||||
avail: 2000000000000,
|
||||
status: "healthy".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&pool).unwrap();
|
||||
let deserialized: CephPool = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(pool.pool, deserialized.pool);
|
||||
assert_eq!(pool.status, "healthy");
|
||||
}
|
||||
}
|
||||
291
src-tauri/src/proxmox/client.rs
Normal file
291
src-tauri/src/proxmox/client.rs
Normal file
@ -0,0 +1,291 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Proxmox VE/PBS API client
|
||||
/// Implements authentication and request handling for Proxmox APIs
|
||||
pub struct ProxmoxClient {
|
||||
base_url: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
api_token: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
/// Authentication response from Proxmox
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
pub ticket: String,
|
||||
pub username: String,
|
||||
pub expire: u64,
|
||||
pub cap: String,
|
||||
}
|
||||
|
||||
/// API token for authentication
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiToken {
|
||||
pub token_id: String,
|
||||
pub name: String,
|
||||
pub expire: u64,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProxmoxClient {
|
||||
/// Create a new Proxmox client
|
||||
pub fn new(base_url: &str, port: u16, username: &str) -> Self {
|
||||
Self {
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
port,
|
||||
username: username.to_string(),
|
||||
api_token: None,
|
||||
client: Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticate with root username and password
|
||||
/// Returns the API ticket for subsequent requests
|
||||
pub async fn authenticate(&self, password: &str) -> Result<String> {
|
||||
let url = format!("{}/api2/json/access/ticket", self.base_url);
|
||||
|
||||
let params = vec![
|
||||
("username", self.username.as_str()),
|
||||
("password", password),
|
||||
];
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Authentication request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow!(
|
||||
"Authentication failed with status {}: {}",
|
||||
status,
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
let auth: AuthResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to parse authentication response: {}", e))?;
|
||||
|
||||
Ok(auth.ticket)
|
||||
}
|
||||
|
||||
/// Authenticate with API token
|
||||
pub fn authenticate_with_token(&mut self, token: &str) {
|
||||
self.api_token = Some(token.to_string());
|
||||
}
|
||||
|
||||
/// Get the full API URL for a given path
|
||||
fn get_api_url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api2/json/{}",
|
||||
self.base_url,
|
||||
path.trim_start_matches('/')
|
||||
)
|
||||
}
|
||||
|
||||
/// Build request headers with authentication
|
||||
fn build_headers(&self, ticket: Option<&str>) -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
||||
if let Some(token) = &self.api_token {
|
||||
// API token format: user@realm!tokenid=tokenvalue
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("PVEAPIAuth {}", token)
|
||||
.parse()
|
||||
.expect("Invalid auth header"),
|
||||
);
|
||||
} else if let Some(ticket) = ticket {
|
||||
// Cookie-based authentication
|
||||
headers.insert(
|
||||
"Cookie",
|
||||
format!("PVEAuthCookie={}", ticket)
|
||||
.parse()
|
||||
.expect("Invalid cookie header"),
|
||||
);
|
||||
}
|
||||
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
"application/x-www-form-urlencoded"
|
||||
.parse()
|
||||
.expect("Invalid content type"),
|
||||
);
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
/// GET request to Proxmox API
|
||||
pub async fn get<T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
ticket: Option<&str>,
|
||||
) -> Result<T> {
|
||||
let url = self.get_api_url(path);
|
||||
let headers = self.build_headers(ticket);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.headers(headers)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("GET request failed: {}", e))?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
/// POST request to Proxmox API
|
||||
pub async fn post<T: for<'de> Deserialize<'de>, B: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
ticket: Option<&str>,
|
||||
) -> Result<T> {
|
||||
let url = self.get_api_url(path);
|
||||
let headers = self.build_headers(ticket);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.headers(headers)
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("POST request failed: {}", e))?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
/// PUT request to Proxmox API
|
||||
pub async fn put<T: for<'de> Deserialize<'de>, B: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
ticket: Option<&str>,
|
||||
) -> Result<T> {
|
||||
let url = self.get_api_url(path);
|
||||
let headers = self.build_headers(ticket);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.put(&url)
|
||||
.headers(headers)
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("PUT request failed: {}", e))?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
/// DELETE request to Proxmox API
|
||||
pub async fn delete<T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
ticket: Option<&str>,
|
||||
) -> Result<T> {
|
||||
let url = self.get_api_url(path);
|
||||
let headers = self.build_headers(ticket);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.delete(&url)
|
||||
.headers(headers)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("DELETE request failed: {}", e))?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
/// Handle API response
|
||||
async fn handle_response<T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
response: reqwest::Response,
|
||||
) -> Result<T> {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow!(
|
||||
"API request failed with status {}: {}",
|
||||
status,
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
let data: HashMap<String, serde_json::Value> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to parse API response: {}", e))?;
|
||||
|
||||
// Proxmox API wraps data in "data" field
|
||||
data.get("data")
|
||||
.ok_or_else(|| anyhow!("Response missing 'data' field"))
|
||||
.and_then(|d| {
|
||||
serde_json::from_value(d.clone())
|
||||
.map_err(|e| anyhow!("Failed to deserialize data: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the base URL
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
/// Get the port
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// Get the username
|
||||
pub fn username(&self) -> &str {
|
||||
&self.username
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_proxmox_client_new() {
|
||||
let client = ProxmoxClient::new("https://pve.example.com", 8006, "root@pam");
|
||||
assert_eq!(client.base_url(), "https://pve.example.com");
|
||||
assert_eq!(client.port(), 8006);
|
||||
assert_eq!(client.username(), "root@pam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxmox_client_with_trailing_slash() {
|
||||
let client = ProxmoxClient::new("https://pve.example.com/", 8006, "root@pam");
|
||||
assert_eq!(client.base_url(), "https://pve.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_api_url() {
|
||||
let client = ProxmoxClient::new("https://pve.example.com", 8006, "root@pam");
|
||||
assert_eq!(
|
||||
client.get_api_url("cluster/resources"),
|
||||
"https://pve.example.com/api2/json/cluster/resources"
|
||||
);
|
||||
assert_eq!(
|
||||
client.get_api_url("/cluster/resources"),
|
||||
"https://pve.example.com/api2/json/cluster/resources"
|
||||
);
|
||||
}
|
||||
}
|
||||
175
src-tauri/src/proxmox/cluster.rs
Normal file
175
src-tauri/src/proxmox/cluster.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Cluster information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub cluster_type: ClusterType,
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Cluster type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ClusterType {
|
||||
#[default]
|
||||
VE, // Proxmox VE
|
||||
PBS, // Proxmox Backup Server
|
||||
}
|
||||
|
||||
/// Cluster registry for managing multiple clusters
|
||||
pub struct ClusterRegistry {
|
||||
clusters: HashMap<String, ClusterInfo>,
|
||||
}
|
||||
|
||||
impl ClusterRegistry {
|
||||
/// Create a new cluster registry
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clusters: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a cluster
|
||||
pub fn add_cluster(&mut self, cluster: ClusterInfo) {
|
||||
self.clusters.insert(cluster.id.clone(), cluster);
|
||||
}
|
||||
|
||||
/// Remove a cluster
|
||||
pub fn remove_cluster(&mut self, id: &str) -> Option<ClusterInfo> {
|
||||
self.clusters.remove(id)
|
||||
}
|
||||
|
||||
/// Get a cluster by ID
|
||||
pub fn get_cluster(&self, id: &str) -> Option<&ClusterInfo> {
|
||||
self.clusters.get(id)
|
||||
}
|
||||
|
||||
/// Get all clusters
|
||||
pub fn list_clusters(&self) -> Vec<&ClusterInfo> {
|
||||
self.clusters.values().collect()
|
||||
}
|
||||
|
||||
/// Get clusters by type
|
||||
pub fn list_clusters_by_type(&self, cluster_type: &ClusterType) -> Vec<&ClusterInfo> {
|
||||
self.clusters
|
||||
.values()
|
||||
.filter(|c| &c.cluster_type == cluster_type)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get cluster count
|
||||
pub fn cluster_count(&self) -> usize {
|
||||
self.clusters.len()
|
||||
}
|
||||
|
||||
/// Check if a cluster exists
|
||||
pub fn has_cluster(&self, id: &str) -> bool {
|
||||
self.clusters.contains_key(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClusterRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cluster_registry_new() {
|
||||
let registry = ClusterRegistry::new();
|
||||
assert_eq!(registry.cluster_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cluster_registry_add_and_get() {
|
||||
let mut registry = ClusterRegistry::new();
|
||||
|
||||
let cluster = ClusterInfo {
|
||||
id: "cluster-1".to_string(),
|
||||
name: "Production".to_string(),
|
||||
cluster_type: ClusterType::VE,
|
||||
url: "https://pve.example.com".to_string(),
|
||||
port: 8006,
|
||||
username: "root@pam".to_string(),
|
||||
created_at: "2026-06-10 12:00:00".to_string(),
|
||||
updated_at: "2026-06-10 12:00:00".to_string(),
|
||||
};
|
||||
|
||||
registry.add_cluster(cluster.clone());
|
||||
assert_eq!(registry.cluster_count(), 1);
|
||||
|
||||
let retrieved = registry.get_cluster("cluster-1");
|
||||
assert!(retrieved.is_some());
|
||||
assert_eq!(retrieved.unwrap().name, "Production");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cluster_registry_remove() {
|
||||
let mut registry = ClusterRegistry::new();
|
||||
|
||||
let cluster = ClusterInfo {
|
||||
id: "cluster-1".to_string(),
|
||||
name: "Production".to_string(),
|
||||
cluster_type: ClusterType::VE,
|
||||
url: "https://pve.example.com".to_string(),
|
||||
port: 8006,
|
||||
username: "root@pam".to_string(),
|
||||
created_at: "2026-06-10 12:00:00".to_string(),
|
||||
updated_at: "2026-06-10 12:00:00".to_string(),
|
||||
};
|
||||
|
||||
registry.add_cluster(cluster);
|
||||
assert_eq!(registry.cluster_count(), 1);
|
||||
|
||||
let removed = registry.remove_cluster("cluster-1");
|
||||
assert!(removed.is_some());
|
||||
assert_eq!(registry.cluster_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cluster_registry_list_by_type() {
|
||||
let mut registry = ClusterRegistry::new();
|
||||
|
||||
let ve_cluster = ClusterInfo {
|
||||
id: "ve-1".to_string(),
|
||||
name: "VE Cluster".to_string(),
|
||||
cluster_type: ClusterType::VE,
|
||||
url: "https://pve.example.com".to_string(),
|
||||
port: 8006,
|
||||
username: "root@pam".to_string(),
|
||||
created_at: "2026-06-10 12:00:00".to_string(),
|
||||
updated_at: "2026-06-10 12:00:00".to_string(),
|
||||
};
|
||||
|
||||
let pbs_cluster = ClusterInfo {
|
||||
id: "pbs-1".to_string(),
|
||||
name: "PBS Cluster".to_string(),
|
||||
cluster_type: ClusterType::PBS,
|
||||
url: "https://pbs.example.com".to_string(),
|
||||
port: 8007,
|
||||
username: "root@pam".to_string(),
|
||||
created_at: "2026-06-10 12:00:00".to_string(),
|
||||
updated_at: "2026-06-10 12:00:00".to_string(),
|
||||
};
|
||||
|
||||
registry.add_cluster(ve_cluster);
|
||||
registry.add_cluster(pbs_cluster);
|
||||
|
||||
let ve_clusters = registry.list_clusters_by_type(&ClusterType::VE);
|
||||
assert_eq!(ve_clusters.len(), 1);
|
||||
|
||||
let pbs_clusters = registry.list_clusters_by_type(&ClusterType::PBS);
|
||||
assert_eq!(pbs_clusters.len(), 1);
|
||||
}
|
||||
}
|
||||
95
src-tauri/src/proxmox/firewall.rs
Normal file
95
src-tauri/src/proxmox/firewall.rs
Normal file
@ -0,0 +1,95 @@
|
||||
// Firewall management module
|
||||
// Provides operations for managing Proxmox firewall
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Firewall rule
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FirewallRule {
|
||||
pub rule_num: u32,
|
||||
pub action: String,
|
||||
pub protocol: String,
|
||||
pub source: String,
|
||||
pub destination: String,
|
||||
pub port: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Firewall status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FirewallStatus {
|
||||
pub enabled: bool,
|
||||
pub rules: Vec<FirewallRule>,
|
||||
pub rule_count: u32,
|
||||
}
|
||||
|
||||
/// List firewall rules
|
||||
pub async fn list_firewall_rules(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<FirewallRule>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Add firewall rule
|
||||
pub async fn add_rule(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_rule: &FirewallRule,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Delete firewall rule
|
||||
pub async fn delete_rule(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_rule_num: u32,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Enable firewall
|
||||
pub async fn enable_firewall(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Disable firewall
|
||||
pub async fn disable_firewall(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_firewall_rule_serialization() {
|
||||
let rule = FirewallRule {
|
||||
rule_num: 1,
|
||||
action: "ACCEPT".to_string(),
|
||||
protocol: "tcp".to_string(),
|
||||
source: "10.0.0.0/8".to_string(),
|
||||
destination: "any".to_string(),
|
||||
port: Some("443".to_string()),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&rule).unwrap();
|
||||
let deserialized: FirewallRule = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(rule.action, deserialized.action);
|
||||
assert_eq!(rule.enabled, deserialized.enabled);
|
||||
}
|
||||
}
|
||||
62
src-tauri/src/proxmox/ha.rs
Normal file
62
src-tauri/src/proxmox/ha.rs
Normal file
@ -0,0 +1,62 @@
|
||||
// HA (High Availability) groups management module
|
||||
// Provides operations for managing Proxmox HA groups
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HA group information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HaGroup {
|
||||
pub group: String,
|
||||
pub nodes: Vec<String>,
|
||||
pub max_failures: u32,
|
||||
pub max_relocate: u32,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// HA resource information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HaResource {
|
||||
pub resource: String,
|
||||
pub group: Option<String>,
|
||||
pub node: Option<String>,
|
||||
pub state: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// List HA groups
|
||||
pub async fn list_ha_groups(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<HaGroup>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List HA resources
|
||||
pub async fn list_ha_resources(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<HaResource>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ha_group_serialization() {
|
||||
let group = HaGroup {
|
||||
group: "primary".to_string(),
|
||||
nodes: vec!["pve-node-1".to_string(), "pve-node-2".to_string()],
|
||||
max_failures: 2,
|
||||
max_relocate: 1,
|
||||
state: "enabled".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&group).unwrap();
|
||||
let deserialized: HaGroup = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(group.group, deserialized.group);
|
||||
assert_eq!(group.state, "enabled");
|
||||
}
|
||||
}
|
||||
87
src-tauri/src/proxmox/metrics.rs
Normal file
87
src-tauri/src/proxmox/metrics.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Node metrics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeMetrics {
|
||||
pub cpu: f64, // CPU usage percentage
|
||||
pub memory: f64, // Memory usage percentage
|
||||
pub disk: f64, // Disk usage percentage
|
||||
pub network: f64, // Network usage percentage
|
||||
pub load: f64, // Load average
|
||||
pub uptime: u64, // Uptime in seconds
|
||||
}
|
||||
|
||||
/// Node status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeStatus {
|
||||
pub node: String,
|
||||
pub cpu: f64,
|
||||
pub memory: f64,
|
||||
pub disk: f64,
|
||||
pub load: f64,
|
||||
pub uptime: u64,
|
||||
pub version: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Get node metrics for a specific node
|
||||
pub async fn get_node_metrics(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<NodeMetrics, String> {
|
||||
// Implementation will be completed in Phase 2
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List all nodes in a cluster
|
||||
pub async fn list_nodes(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<NodeStatus>, String> {
|
||||
// Implementation will be completed in Phase 2
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_node_metrics_serialization() {
|
||||
let metrics = NodeMetrics {
|
||||
cpu: 42.5,
|
||||
memory: 65.3,
|
||||
disk: 30.1,
|
||||
network: 15.8,
|
||||
load: 2.5,
|
||||
uptime: 86400,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&metrics).unwrap();
|
||||
let deserialized: NodeMetrics = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(metrics.cpu, deserialized.cpu);
|
||||
assert_eq!(metrics.memory, deserialized.memory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_status_serialization() {
|
||||
let status = NodeStatus {
|
||||
node: "pve-node-1".to_string(),
|
||||
cpu: 42.5,
|
||||
memory: 65.3,
|
||||
disk: 30.1,
|
||||
load: 2.5,
|
||||
uptime: 86400,
|
||||
version: "7.4-15".to_string(),
|
||||
status: "online".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
let deserialized: NodeStatus = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(status.node, deserialized.node);
|
||||
assert_eq!(status.status, "online");
|
||||
}
|
||||
}
|
||||
18
src-tauri/src/proxmox/mod.rs
Normal file
18
src-tauri/src/proxmox/mod.rs
Normal file
@ -0,0 +1,18 @@
|
||||
// Proxmox integration module
|
||||
// Provides management for Proxmox VE and Proxmox Backup Server clusters
|
||||
|
||||
pub mod backup;
|
||||
pub mod ceph;
|
||||
pub mod client;
|
||||
pub mod cluster;
|
||||
pub mod firewall;
|
||||
pub mod ha;
|
||||
pub mod metrics;
|
||||
pub mod node;
|
||||
pub mod sdn;
|
||||
pub mod storage;
|
||||
pub mod updates;
|
||||
pub mod vm;
|
||||
|
||||
pub use client::ProxmoxClient;
|
||||
pub use cluster::{ClusterInfo, ClusterRegistry, ClusterType};
|
||||
59
src-tauri/src/proxmox/node.rs
Normal file
59
src-tauri/src/proxmox/node.rs
Normal file
@ -0,0 +1,59 @@
|
||||
// Node management module
|
||||
// Provides operations for managing Proxmox nodes
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Node information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeInfo {
|
||||
pub node: String,
|
||||
pub cpu: f64,
|
||||
pub memory: f64,
|
||||
pub disk: f64,
|
||||
pub load: f64,
|
||||
pub uptime: u64,
|
||||
pub version: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List all nodes
|
||||
pub async fn list_nodes(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<NodeInfo>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Get node status
|
||||
pub async fn get_node_status(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<NodeInfo, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_node_info_serialization() {
|
||||
let node = NodeInfo {
|
||||
node: "pve-node-1".to_string(),
|
||||
cpu: 0.42,
|
||||
memory: 0.65,
|
||||
disk: 0.30,
|
||||
load: 2.5,
|
||||
uptime: 86400,
|
||||
version: "7.4-15".to_string(),
|
||||
status: "online".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&node).unwrap();
|
||||
let deserialized: NodeInfo = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(node.node, deserialized.node);
|
||||
assert_eq!(node.status, "online");
|
||||
}
|
||||
}
|
||||
73
src-tauri/src/proxmox/sdn.rs
Normal file
73
src-tauri/src/proxmox/sdn.rs
Normal file
@ -0,0 +1,73 @@
|
||||
// SDN (Software-Defined Networking) management module
|
||||
// Provides operations for managing Proxmox SDN
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// EVPN zone information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EvpnZone {
|
||||
pub zone: String,
|
||||
pub asn: u32,
|
||||
pub vni: u32,
|
||||
pub gateways: Vec<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Virtual network information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VirtualNetwork {
|
||||
pub vnet: String,
|
||||
pub zone: String,
|
||||
pub l2vni: u32,
|
||||
pub dhcp: bool,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List EVPN zones
|
||||
pub async fn list_evpn_zones(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<EvpnZone>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Create EVPN zone
|
||||
pub async fn create_evpn_zone(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_zone: &str,
|
||||
_asn: u32,
|
||||
_vni: u32,
|
||||
_ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List virtual networks
|
||||
pub async fn list_vnets(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<VirtualNetwork>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_evpn_zone_serialization() {
|
||||
let zone = EvpnZone {
|
||||
zone: "primary".to_string(),
|
||||
asn: 65001,
|
||||
vni: 1000,
|
||||
gateways: vec!["10.0.0.1".to_string()],
|
||||
status: "active".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&zone).unwrap();
|
||||
let deserialized: EvpnZone = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(zone.zone, deserialized.zone);
|
||||
assert_eq!(zone.status, "active");
|
||||
}
|
||||
}
|
||||
61
src-tauri/src/proxmox/storage.rs
Normal file
61
src-tauri/src/proxmox/storage.rs
Normal file
@ -0,0 +1,61 @@
|
||||
// Storage management module
|
||||
// Provides operations for managing Proxmox storage
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Storage information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageInfo {
|
||||
pub storage: String,
|
||||
pub node: String,
|
||||
pub type_: String,
|
||||
pub content: String,
|
||||
pub size: u64,
|
||||
pub used: u64,
|
||||
pub available: u64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List all storages
|
||||
pub async fn list_storages(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<StorageInfo>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// Get storage status
|
||||
pub async fn get_storage_status(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_node: &str,
|
||||
_storage: &str,
|
||||
_ticket: &str,
|
||||
) -> Result<StorageInfo, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_storage_info_serialization() {
|
||||
let storage = StorageInfo {
|
||||
storage: "local".to_string(),
|
||||
node: "pve-node-1".to_string(),
|
||||
type_: "dir".to_string(),
|
||||
content: "images,backup,iso".to_string(),
|
||||
size: 1000000000000,
|
||||
used: 300000000000,
|
||||
available: 700000000000,
|
||||
status: "available".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&storage).unwrap();
|
||||
let deserialized: StorageInfo = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(storage.storage, deserialized.storage);
|
||||
assert_eq!(storage.status, "available");
|
||||
}
|
||||
}
|
||||
58
src-tauri/src/proxmox/updates.rs
Normal file
58
src-tauri/src/proxmox/updates.rs
Normal file
@ -0,0 +1,58 @@
|
||||
// Update management module
|
||||
// Provides operations for managing Proxmox updates
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Update information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub package: String,
|
||||
pub version: String,
|
||||
pub available_version: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
/// Update status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateStatus {
|
||||
pub checked_at: String,
|
||||
pub updates: Vec<UpdateInfo>,
|
||||
pub update_count: u32,
|
||||
}
|
||||
|
||||
/// Check for updates
|
||||
pub async fn check_updates(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<UpdateStatus, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
/// List available updates
|
||||
pub async fn list_updates(
|
||||
_client: &crate::proxmox::client::ProxmoxClient,
|
||||
_ticket: &str,
|
||||
) -> Result<Vec<UpdateInfo>, String> {
|
||||
Err("Not implemented yet".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_update_info_serialization() {
|
||||
let update = UpdateInfo {
|
||||
package: "proxmox-ve".to_string(),
|
||||
version: "7.4-15".to_string(),
|
||||
available_version: "7.4-16".to_string(),
|
||||
size: 50000000,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&update).unwrap();
|
||||
let deserialized: UpdateInfo = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(update.package, deserialized.package);
|
||||
assert_eq!(update.available_version, deserialized.available_version);
|
||||
}
|
||||
}
|
||||
438
src-tauri/src/proxmox/vm.rs
Normal file
438
src-tauri/src/proxmox/vm.rs
Normal file
@ -0,0 +1,438 @@
|
||||
// VM management module
|
||||
// Provides operations for managing Proxmox VE virtual machines
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// VM information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VmInfo {
|
||||
pub id: u32,
|
||||
pub name: Option<String>,
|
||||
pub status: String,
|
||||
pub cpu: f64,
|
||||
pub memory: u64,
|
||||
pub disk: u64,
|
||||
pub uptime: u64,
|
||||
pub node: String,
|
||||
pub template: Option<bool>,
|
||||
pub agent: Option<String>,
|
||||
pub mem: Option<u64>,
|
||||
pub max_mem: Option<u64>,
|
||||
pub max_disk: Option<u64>,
|
||||
pub netin: Option<u64>,
|
||||
pub netout: Option<u64>,
|
||||
pub diskread: Option<u64>,
|
||||
pub diskwrite: Option<u64>,
|
||||
}
|
||||
|
||||
/// VM power state
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VmState {
|
||||
Running,
|
||||
Stopped,
|
||||
Suspended,
|
||||
Paused,
|
||||
}
|
||||
|
||||
/// Start a VM
|
||||
pub async fn start_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/start", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop a VM
|
||||
pub async fn stop_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/stop", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to stop VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reboot a VM
|
||||
pub async fn reboot_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/reboot", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to reboot VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown a VM
|
||||
pub async fn shutdown_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/shutdown", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to shutdown VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume a suspended VM
|
||||
pub async fn resume_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/resume", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to resume VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Suspend a VM
|
||||
pub async fn suspend_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/suspend", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to suspend VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all VMs
|
||||
pub async fn list_vms(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
ticket: &str,
|
||||
) -> Result<Vec<VmInfo>, String> {
|
||||
let path = "cluster/resources";
|
||||
let params = serde_json::json!({
|
||||
"type": "qemu"
|
||||
});
|
||||
|
||||
let response: serde_json::Value = client
|
||||
.post(path, ¶ms, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list VMs: {}", e))?;
|
||||
|
||||
// Parse the response to extract VM info
|
||||
// The API returns a list of resources in the "data" field
|
||||
if let Some(resources) = response.get("data").and_then(|d| d.as_array()) {
|
||||
let vms: Vec<VmInfo> = resources
|
||||
.iter()
|
||||
.filter_map(|r| {
|
||||
let vmid = r.get("vmid")?.as_u64()?;
|
||||
let node = r.get("node")?.as_str()?.to_string();
|
||||
let name = r.get("name")?.as_str().map(|s| s.to_string());
|
||||
let status = r.get("status")?.as_str()?.to_string();
|
||||
let cpu = r.get("cpu")?.as_f64()?;
|
||||
|
||||
Some(VmInfo {
|
||||
id: vmid as u32,
|
||||
name,
|
||||
status,
|
||||
cpu,
|
||||
memory: r.get("mem")?.as_u64().unwrap_or(0),
|
||||
disk: r.get("disk")?.as_u64().unwrap_or(0),
|
||||
uptime: r.get("uptime")?.as_u64().unwrap_or(0),
|
||||
node,
|
||||
template: r.get("template")?.as_bool(),
|
||||
agent: r.get("agent")?.as_str().map(|s| s.to_string()),
|
||||
mem: r.get("mem")?.as_u64(),
|
||||
max_mem: r.get("maxmem")?.as_u64(),
|
||||
max_disk: r.get("maxdisk")?.as_u64(),
|
||||
netin: r.get("netin")?.as_u64(),
|
||||
netout: r.get("netout")?.as_u64(),
|
||||
diskread: r.get("diskread")?.as_u64(),
|
||||
diskwrite: r.get("diskwrite")?.as_u64(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(vms)
|
||||
} else {
|
||||
Err("Invalid response format: missing 'data' field".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get VM details
|
||||
pub async fn get_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<VmInfo, String> {
|
||||
let path = format!("nodes/{}/qemu/{}/config", node, vmid);
|
||||
let response: serde_json::Value = client
|
||||
.get(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get VM {}: {}", vmid, e))?;
|
||||
|
||||
// Parse the response to extract VM info
|
||||
let vm = response.get("data").ok_or("Invalid response format")?;
|
||||
|
||||
Ok(VmInfo {
|
||||
id: vmid,
|
||||
name: vm.get("name")?.as_str().map(|s| s.to_string()),
|
||||
status: vm.get("status")?.as_str().unwrap_or("unknown").to_string(),
|
||||
cpu: vm.get("cpu")?.as_f64().unwrap_or(0.0),
|
||||
memory: vm.get("memory")?.as_u64().unwrap_or(0),
|
||||
disk: vm.get("disk")?.as_u64().unwrap_or(0),
|
||||
uptime: vm.get("uptime")?.as_u64().unwrap_or(0),
|
||||
node: node.to_string(),
|
||||
template: vm.get("template")?.as_bool(),
|
||||
agent: vm.get("agent")?.as_str().map(|s| s.to_string()),
|
||||
mem: vm.get("mem")?.as_u64(),
|
||||
max_mem: vm.get("maxmem")?.as_u64(),
|
||||
max_disk: vm.get("maxdisk")?.as_u64(),
|
||||
netin: vm.get("netin")?.as_u64(),
|
||||
netout: vm.get("netout")?.as_u64(),
|
||||
diskread: vm.get("diskread")?.as_u64(),
|
||||
diskwrite: vm.get("diskwrite")?.as_u64(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get VM status
|
||||
pub async fn get_vm_status(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let path = format!("nodes/{}/qemu/{}/status/current", node, vmid);
|
||||
client
|
||||
.get(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get VM status {}: {}", vmid, e))
|
||||
}
|
||||
|
||||
/// Get VM current configuration
|
||||
pub async fn get_vm_config(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let path = format!("nodes/{}/qemu/{}/config", node, vmid);
|
||||
client
|
||||
.get(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get VM config {}: {}", vmid, e))
|
||||
}
|
||||
|
||||
/// Create a new VM
|
||||
pub async fn create_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
config: &serde_json::Value,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu", node);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, config, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a VM
|
||||
pub async fn delete_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}", node, vmid);
|
||||
let _response: serde_json::Value = client
|
||||
.delete(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete VM {}: {}", vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clone a VM
|
||||
pub async fn clone_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
new_vmid: u32,
|
||||
name: &str,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/clone", node, vmid);
|
||||
let config = serde_json::json!({
|
||||
"newid": new_vmid,
|
||||
"name": name,
|
||||
"full": 1
|
||||
});
|
||||
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &config, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to clone VM {} to {}: {}", vmid, new_vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrate a VM
|
||||
pub async fn migrate_vm(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
source_node: &str,
|
||||
vmid: u32,
|
||||
target_node: &str,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/migrate", source_node, vmid);
|
||||
let config = serde_json::json!({
|
||||
"target": target_node,
|
||||
"online": true
|
||||
});
|
||||
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &config, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to migrate VM {} to {}: {}", vmid, target_node, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a snapshot
|
||||
pub async fn create_snapshot(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
snapshot_name: &str,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/snapshot", node, vmid);
|
||||
let config = serde_json::json!({
|
||||
"snapname": snapshot_name
|
||||
});
|
||||
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &config, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create snapshot {} for VM {}: {}", snapshot_name, vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a snapshot
|
||||
pub async fn delete_snapshot(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
snapshot_name: &str,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/snapshot/{}", node, vmid, snapshot_name);
|
||||
let _response: serde_json::Value = client
|
||||
.delete(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete snapshot {} for VM {}: {}", snapshot_name, vmid, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rollback to a snapshot
|
||||
pub async fn rollback_snapshot(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
snapshot_name: &str,
|
||||
ticket: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("nodes/{}/qemu/{}/snapshot/{}/rollback", node, vmid, snapshot_name);
|
||||
let _response: serde_json::Value = client
|
||||
.post(&path, &serde_json::json!({}), ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to rollback VM {} to snapshot {}: {}", vmid, snapshot_name, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List snapshots
|
||||
pub async fn list_snapshots(
|
||||
client: &crate::proxmox::client::ProxmoxClient,
|
||||
node: &str,
|
||||
vmid: u32,
|
||||
ticket: &str,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let path = format!("nodes/{}/qemu/{}/snapshot", node, vmid);
|
||||
let response: serde_json::Value = client
|
||||
.get(&path, ticket)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list snapshots for VM {}: {}", vmid, e))?;
|
||||
|
||||
if let Some(snapshots) = response.get("data").and_then(|d| d.as_array()) {
|
||||
Ok(snapshots.to_vec())
|
||||
} else {
|
||||
Err("Invalid response format: missing 'data' field".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vm_info_serialization() {
|
||||
let vm = VmInfo {
|
||||
id: 100,
|
||||
name: Some("web-server".to_string()),
|
||||
status: "running".to_string(),
|
||||
cpu: 2.5,
|
||||
memory: 4096,
|
||||
disk: 50000,
|
||||
uptime: 86400,
|
||||
node: "pve-node-1".to_string(),
|
||||
template: Some(false),
|
||||
agent: Some("1".to_string()),
|
||||
mem: Some(4096),
|
||||
max_mem: Some(8192),
|
||||
max_disk: Some(100000),
|
||||
netin: Some(1000000),
|
||||
netout: Some(2000000),
|
||||
diskread: Some(5000000),
|
||||
diskwrite: Some(3000000),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&vm).unwrap();
|
||||
let deserialized: VmInfo = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(vm.id, deserialized.id);
|
||||
assert_eq!(vm.name, deserialized.name);
|
||||
assert_eq!(vm.status, "running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vm_state_serialization() {
|
||||
let json = serde_json::to_string(&VmState::Running).unwrap();
|
||||
assert_eq!(json, "\"running\"");
|
||||
|
||||
let running: VmState = serde_json::from_str("\"running\"").unwrap();
|
||||
assert_eq!(running, VmState::Running);
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,9 @@ pub struct AppState {
|
||||
Arc<TokioMutex<HashMap<String, tokio::sync::oneshot::Sender<ApprovalResponse>>>>,
|
||||
/// Kubernetes cluster clients: cluster_id -> client
|
||||
pub clusters: Arc<TokioMutex<HashMap<String, crate::kube::ClusterClient>>>,
|
||||
/// Proxmox cluster clients: cluster_id -> client
|
||||
pub proxmox_clusters:
|
||||
Arc<TokioMutex<HashMap<String, Arc<TokioMutex<crate::proxmox::client::ProxmoxClient>>>>>,
|
||||
/// Port forwarding sessions: session_id -> session
|
||||
pub port_forwards: Arc<TokioMutex<HashMap<String, crate::kube::PortForwardSession>>>,
|
||||
/// Refresh registry for domain-based data fetching
|
||||
|
||||
Loading…
Reference in New Issue
Block a user