Files
proxmox-task/docs/superpowers/plans/2026-04-10-proxmox-vm-gui.md
2026-04-10 22:44:00 +08:00

9.6 KiB
Raw Blame History

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

[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 骨架
#![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
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 函数

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
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
#![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 添加静态链接
[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?