422 lines
9.6 KiB
Markdown
422 lines
9.6 KiB
Markdown
|
|
# Proxmox VM GUI 控制工具实现计划
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 将 Python Proxmox 控制器改写为 Rust GUI 应用,使用 egui 框架,通过 Proxmox API 控制 Windows VM
|
|||
|
|
|
|||
|
|
**Architecture:** 单窗口 GUI 应用,顶部显示状态和控制按钮,底部显示操作日志
|
|||
|
|
|
|||
|
|
**Tech Stack:**
|
|||
|
|
- GUI: egui 0.29
|
|||
|
|
- HTTP: reqwest (rust-tls)
|
|||
|
|
- 配置: dotenv + serde
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1: 创建 Rust 项目结构
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `Cargo.toml`
|
|||
|
|
- Create: `src/main.rs`
|
|||
|
|
- Create: `.env.example`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 Cargo.toml**
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
[package]
|
|||
|
|
name = "proxmox-vm-gui"
|
|||
|
|
version = "0.1.0"
|
|||
|
|
edition = "2021"
|
|||
|
|
|
|||
|
|
[dependencies]
|
|||
|
|
egui = "0.29"
|
|||
|
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
|||
|
|
serde = { version = "1.0", features = ["derive"] }
|
|||
|
|
serde_json = "1.0"
|
|||
|
|
dotenv = "0.15"
|
|||
|
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
|||
|
|
chrono = "0.4"
|
|||
|
|
|
|||
|
|
[profile.release]
|
|||
|
|
opt-level = 3
|
|||
|
|
lto = true
|
|||
|
|
codegen-units = 1
|
|||
|
|
|
|||
|
|
[target.x86_64-pc-windows-gnu]
|
|||
|
|
linker = "x86_64-w64-mingw32-gcc"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 创建 src/main.rs 骨架**
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
#![windows_subsystem = "windows"]
|
|||
|
|
|
|||
|
|
fn main() {
|
|||
|
|
println!("Hello");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 创建 .env.example**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PROXMOX_HOST=proxmox.example.com
|
|||
|
|
PROXMOX_USER=root@pam
|
|||
|
|
PROXMOX_TOKEN=your-api-token-here
|
|||
|
|
VM_ID=100
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 验证编译**
|
|||
|
|
|
|||
|
|
Run: `cargo build --target x86_64-pc-windows-gnu`
|
|||
|
|
Expected: 编译成功生成 exe
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2: 实现 Proxmox API 客户端
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `src/api.rs`
|
|||
|
|
- Modify: `src/main.rs`
|
|||
|
|
|
|||
|
|
Proxmox API 基础结构,用于所有 API 调用
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 src/api.rs**
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
use serde::{Deserialize, Serialize};
|
|||
|
|
use std::sync::Arc;
|
|||
|
|
use tokio::sync::RwLock;
|
|||
|
|
|
|||
|
|
#[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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 修改 main.rs 添加基础结构**
|
|||
|
|
|
|||
|
|
导入 api 模块,添加空 main 函数
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
mod api;
|
|||
|
|
use api::ProxmoxClient;
|
|||
|
|
|
|||
|
|
fn main() {
|
|||
|
|
println!("Proxmox VM GUI");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 测试 API 编译**
|
|||
|
|
|
|||
|
|
Run: `cargo build --target x86_64-pc-windows-gnu`
|
|||
|
|
Expected: 编译成功
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3: 实现 egui GUI 界面
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `src/gui.rs`
|
|||
|
|
- Modify: `src/main.rs`
|
|||
|
|
|
|||
|
|
核心 GUI 界面:状态显示、控制按钮、日志区域
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 src/gui.rs**
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
use egui::*;
|
|||
|
|
use crate::api::ProxmoxClient;
|
|||
|
|
use std::sync::Arc;
|
|||
|
|
use tokio::sync::RwLock;
|
|||
|
|
|
|||
|
|
pub struct AppState {
|
|||
|
|
pub client: Arc<RwLock<Option<ProxmoxClient>>>,
|
|||
|
|
pub vm_id: u32,
|
|||
|
|
pub node: String,
|
|||
|
|
pub vm_status: String,
|
|||
|
|
pub logs: Vec<String>,
|
|||
|
|
pub is_connected: bool,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl AppState {
|
|||
|
|
pub fn new() -> Self {
|
|||
|
|
Self {
|
|||
|
|
client: Arc::new(RwLock::new(None)),
|
|||
|
|
vm_id: 100,
|
|||
|
|
node: "proxmox".to_string(),
|
|||
|
|
vm_status: "未知".to_string(),
|
|||
|
|
logs: vec!["程序启动".to_string()],
|
|||
|
|
is_connected: false,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn add_log(&mut self, msg: &str) {
|
|||
|
|
let time = chrono::Local::now().format("%H:%M:%S").to_string();
|
|||
|
|
self.logs.push(format!("{} {}", time, msg));
|
|||
|
|
if self.logs.len() > 100 {
|
|||
|
|
self.logs.remove(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn gui_run() {
|
|||
|
|
let options = NativeOptions {
|
|||
|
|
default_viewport: egui::ViewportBuilder::default()
|
|||
|
|
.with_inner_size([500.0, 400.0])
|
|||
|
|
.with_min_inner_size([400.0, 300.0])
|
|||
|
|
.with_title("Proxmox VM 控制器"),
|
|||
|
|
..Default::default()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
run_native("Proxmox VM 控制器", options, |ctx| {
|
|||
|
|
set_font(ctx);
|
|||
|
|
App::new().clone().run(ctx, &mut AppState::new());
|
|||
|
|
}).unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn set_font(ctx: &egui::Context) {
|
|||
|
|
let mut style = (*ctx.style()).clone();
|
|||
|
|
style.text_styles = [
|
|||
|
|
(TextStyle::Heading, FontId::new(18.0, "Microsoft YaHei")),
|
|||
|
|
(TextStyle::Body, FontId::new(14.0, "Microsoft YaHei")),
|
|||
|
|
(TextStyle::Button, FontId::new(14.0, "Microsoft YaHei")),
|
|||
|
|
].into();
|
|||
|
|
ctx.set_style(style);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct App;
|
|||
|
|
|
|||
|
|
impl App {
|
|||
|
|
fn new() -> Self {
|
|||
|
|
Self
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn run(self, ctx: &egui::Context, state: &mut AppState) {
|
|||
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|||
|
|
self.ui_content(ui, state);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn ui_content(&self, ui: &mut Ui, state: &mut AppState) {
|
|||
|
|
ui.heading("Proxmox VM 控制器");
|
|||
|
|
ui.separator();
|
|||
|
|
|
|||
|
|
ui.horizontal(|ui| {
|
|||
|
|
ui.label("连接状态: ");
|
|||
|
|
ui.label(if state.is_connected { "● 已<><E5B7B2>接" } else { "○ 未连接" });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ui.horizontal(|ui| {
|
|||
|
|
ui.label("VM ID: ");
|
|||
|
|
ui.add(egui::DragValue::new(&mut state.vm_id).clamp_range(100..=999));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ui.horizontal(|ui| {
|
|||
|
|
ui.label("VM 状态: ");
|
|||
|
|
ui.label(&state.vm_status);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ui.separator();
|
|||
|
|
|
|||
|
|
ui.horizontal(|ui| {
|
|||
|
|
if ui.button("启动").clicked() {
|
|||
|
|
state.add_log("点击了启动按钮");
|
|||
|
|
}
|
|||
|
|
if ui.button("停止").clicked() {
|
|||
|
|
state.add_log("点击了停止按钮");
|
|||
|
|
}
|
|||
|
|
if ui.button("刷新").clicked() {
|
|||
|
|
state.add_log("点击了刷新按钮");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ui.separator();
|
|||
|
|
|
|||
|
|
ui.label("日志:");
|
|||
|
|
egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
|||
|
|
for log in &state.logs {
|
|||
|
|
ui.label(log);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 修改 main.rs**
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
#![windows_subsystem = "windows"]
|
|||
|
|
|
|||
|
|
mod api;
|
|||
|
|
mod gui;
|
|||
|
|
|
|||
|
|
fn main() {
|
|||
|
|
gui::gui_run();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 测试 GUI 编译**
|
|||
|
|
|
|||
|
|
Run: `cargo build --target x86_64-pc-windows-gnu`
|
|||
|
|
Expected: 编译成功
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4: 集成配置加载
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/gui.rs`
|
|||
|
|
- Create: `.env`
|
|||
|
|
|
|||
|
|
从 .env 文件加载配置
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 .env 文件**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PROXMOX_HOST=proxmox.example.com
|
|||
|
|
PROXMOX_USER=root@pam
|
|||
|
|
PROXMOX_TOKEN=your-token
|
|||
|
|
VM_ID=100
|
|||
|
|
NODE=proxmox
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 修改gui.rs 添加配置加载**
|
|||
|
|
|
|||
|
|
添加启动时加载配置的代码
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 验证配置加载**
|
|||
|
|
|
|||
|
|
编译测试
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 5: 连接 API 控制逻辑
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/gui.rs`
|
|||
|
|
|
|||
|
|
将按钮与 API 调用关联
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 添加异步任务处理**
|
|||
|
|
|
|||
|
|
在按钮点击时调用 API
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 添加 VM 状态刷新**
|
|||
|
|
|
|||
|
|
刷新按钮获取最新状态
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 测试完整流程**
|
|||
|
|
|
|||
|
|
编译运行测试
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6: 编译发布版本
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `Cargo.toml`
|
|||
|
|
|
|||
|
|
静态链接配置
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 更新 Cargo.toml 添加静态链接**
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
[dependencies]
|
|||
|
|
rustls = "0.23"
|
|||
|
|
flate2 = "1.0"
|
|||
|
|
|
|||
|
|
[target.x86_64-pc-windows-gnu]
|
|||
|
|
rustflags = "-C link-args=-static"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编译发布版本**
|
|||
|
|
|
|||
|
|
Run: `cargo build --release --target x86_64-pc-windows-gnu`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 验证单个 exe**
|
|||
|
|
|
|||
|
|
检查是否生成单个 exe 文件
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Plan complete and saved to `docs/superpowers/plans/2026-04-10-proxmox-vm-gui.md`. Two execution options:**
|
|||
|
|
|
|||
|
|
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
|||
|
|
|
|||
|
|
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
|||
|
|
|
|||
|
|
**Which approach?**
|