feat: add Proxmox VM GUI controller in Rust
This commit is contained in:
80
src/api.rs
Normal file
80
src/api.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxmoxClient {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
token_id: String,
|
||||
token_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VmStatus {
|
||||
pub status: String,
|
||||
pub uptime: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
impl ProxmoxClient {
|
||||
pub fn new(host: &str, token_id: &str, token_secret: &str) -> Self {
|
||||
let base_url = format!("https://{}:8006/api2/json", host);
|
||||
Self {
|
||||
client: reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
base_url,
|
||||
token_id: token_id.to_string(),
|
||||
token_secret: token_secret.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_header(&self) -> String {
|
||||
format!("PVEAPIToken={}={}", self.token_id, self.token_secret)
|
||||
}
|
||||
|
||||
pub async fn get_vm_status(&self, node: &str, vm_id: u32) -> Result<String, String> {
|
||||
let url = format!("{}/nodes/{}/qemu/{}/status/current", self.base_url, node, vm_id);
|
||||
let resp = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let data: ApiResponse<VmStatus> = resp.json().await.map_err(|e| e.to_string())?;
|
||||
Ok(data.data.map(|d| d.status).unwrap_or_else(|| "unknown".to_string()))
|
||||
}
|
||||
|
||||
pub async fn start_vm(&self, node: &str, vm_id: u32) -> Result<(), String> {
|
||||
let url = format!("{}/nodes/{}/qemu/{}/status/start", self.base_url, node, vm_id);
|
||||
self.client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_vm(&self, node: &str, vm_id: u32) -> Result<(), String> {
|
||||
let url = format!("{}/nodes/{}/qemu/{}/status/stop", self.base_url, node, vm_id);
|
||||
self.client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reboot_vm(&self, node: &str, vm_id: u32) -> Result<(), String> {
|
||||
self.stop_vm(node, vm_id).await?;
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
self.start_vm(node, vm_id).await
|
||||
}
|
||||
}
|
||||
234
src/gui.rs
Normal file
234
src/gui.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use eframe::{NativeOptions, run_native};
|
||||
use egui::*;
|
||||
use crate::api::ProxmoxClient;
|
||||
use std::sync::{Arc, RwLock, Mutex};
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::thread;
|
||||
|
||||
pub type SharedState = Arc<RwLock<AppState>>;
|
||||
|
||||
pub struct AppState {
|
||||
pub client: Arc<Mutex<Option<ProxmoxClient>>>,
|
||||
pub vm_id: u32,
|
||||
pub node: String,
|
||||
pub vm_status: String,
|
||||
pub log_buffer: Vec<String>,
|
||||
pub is_connected: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
dotenv().ok();
|
||||
|
||||
let host = env::var("PROXMOX_HOST").unwrap_or_default();
|
||||
let token = env::var("PROXMOX_TOKEN").unwrap_or_default();
|
||||
let vm_id: u32 = env::var("VM_ID").unwrap_or_default().parse().unwrap_or(100);
|
||||
let node = env::var("NODE").unwrap_or_else(|_| "proxmox".to_string());
|
||||
|
||||
let client = if !host.is_empty() && !token.is_empty() {
|
||||
let (token_id, token_secret) = if let Some(pos) = token.find('=') {
|
||||
(token[..pos].to_string(), token[pos+1..].to_string())
|
||||
} else {
|
||||
(token.clone(), token.clone())
|
||||
};
|
||||
let c = ProxmoxClient::new(&host, &token_id, &token_secret);
|
||||
Arc::new(Mutex::new(Some(c)))
|
||||
} else {
|
||||
Arc::new(Mutex::new(None))
|
||||
};
|
||||
|
||||
let is_connected = !host.is_empty() && !token.is_empty();
|
||||
|
||||
Self {
|
||||
client,
|
||||
vm_id,
|
||||
node,
|
||||
vm_status: "未知".to_string(),
|
||||
log_buffer: vec!["程序启动".to_string()],
|
||||
is_connected,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_log(&mut self, msg: &str) {
|
||||
let time = chrono::Local::now().format("%H:%M:%S").to_string();
|
||||
self.log_buffer.push(format!("{} {}", time, msg));
|
||||
if self.log_buffer.len() > 100 {
|
||||
self.log_buffer.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gui_run() {
|
||||
let options = NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([500.0, 450.0])
|
||||
.with_min_inner_size([400.0, 300.0])
|
||||
.with_title("Proxmox VM 控制器"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
run_native("Proxmox VM 控制器", options, Box::new(|cc| {
|
||||
set_font(&cc.egui_ctx);
|
||||
Ok(Box::new(App::new()))
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
fn set_font(ctx: &egui::Context) {
|
||||
let mut style = (*ctx.style()).clone();
|
||||
style.text_styles = [
|
||||
(TextStyle::Heading, FontId::new(18.0, FontFamily::Proportional)),
|
||||
(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional)),
|
||||
(TextStyle::Button, FontId::new(14.0, FontFamily::Proportional)),
|
||||
].into();
|
||||
ctx.set_style(style);
|
||||
}
|
||||
|
||||
struct App {
|
||||
state: SharedState,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: Arc::new(RwLock::new(AppState::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let state = self.state.clone();
|
||||
let ctx_clone = ctx.clone();
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Proxmox VM 控制器");
|
||||
ui.separator();
|
||||
|
||||
let mut st = state.write().unwrap();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("连接状态: ");
|
||||
ui.label(if st.is_connected { "● 已连接" } else { "○ 未连接" });
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("VM ID: ");
|
||||
ui.add(egui::DragValue::new(&mut st.vm_id).range(100..=999));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("节点: ");
|
||||
ui.text_edit_singleline(&mut st.node);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("VM 状态: ");
|
||||
ui.label(&st.vm_status);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let vm_id_start = st.vm_id;
|
||||
let node_start = st.node.clone();
|
||||
let client_start = st.client.clone();
|
||||
|
||||
let vm_id_stop = st.vm_id;
|
||||
let node_stop = st.node.clone();
|
||||
let client_stop = st.client.clone();
|
||||
|
||||
let vm_id_refresh = st.vm_id;
|
||||
let node_refresh = st.node.clone();
|
||||
let client_refresh = st.client.clone();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("启动").clicked() {
|
||||
let client = client_start.clone();
|
||||
let node = node_start.clone();
|
||||
let vm_id = vm_id_start;
|
||||
let state = state.clone();
|
||||
let ctx = ctx_clone.clone();
|
||||
st.add_log("正在启动 VM...");
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async {
|
||||
let client = client.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
c.start_vm(&node, vm_id).await
|
||||
} else {
|
||||
Err("未连接".to_string())
|
||||
}
|
||||
});
|
||||
let msg = match result {
|
||||
Ok(_) => "启动命令已发送".to_string(),
|
||||
Err(e) => format!("启动失败: {}", e),
|
||||
};
|
||||
state.write().unwrap().add_log(&msg);
|
||||
});
|
||||
}
|
||||
|
||||
if ui.button("停止").clicked() {
|
||||
let client = client_stop.clone();
|
||||
let node = node_stop.clone();
|
||||
let vm_id = vm_id_stop;
|
||||
let state = state.clone();
|
||||
let ctx = ctx_clone.clone();
|
||||
st.add_log("正在停止 VM...");
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async {
|
||||
let client = client.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
c.stop_vm(&node, vm_id).await
|
||||
} else {
|
||||
Err("未连接".to_string())
|
||||
}
|
||||
});
|
||||
let msg = match result {
|
||||
Ok(_) => "停止命令已发送".to_string(),
|
||||
Err(e) => format!("停止失败: {}", e),
|
||||
};
|
||||
state.write().unwrap().add_log(&msg);
|
||||
});
|
||||
}
|
||||
|
||||
if ui.button("刷新").clicked() {
|
||||
let client = client_refresh.clone();
|
||||
let node = node_refresh.clone();
|
||||
let vm_id = vm_id_refresh;
|
||||
let state = state.clone();
|
||||
let ctx = ctx_clone.clone();
|
||||
st.add_log("正在获取状态...");
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async {
|
||||
let client = client.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
c.get_vm_status(&node, vm_id).await
|
||||
} else {
|
||||
Err("未连接".to_string())
|
||||
}
|
||||
});
|
||||
let msg = match result {
|
||||
Ok(status) => format!("VM状态: {}", status),
|
||||
Err(e) => format!("获取状态失败: {}", e),
|
||||
};
|
||||
state.write().unwrap().add_log(&msg);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("日志:");
|
||||
egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
||||
for log in &st.log_buffer {
|
||||
ui.label(log);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
8
src/main.rs
Normal file
8
src/main.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod api;
|
||||
mod gui;
|
||||
|
||||
fn main() {
|
||||
gui::gui_run();
|
||||
}
|
||||
Reference in New Issue
Block a user