Compare commits

...

5 Commits

Author SHA1 Message Date
xiaji
e348dbc075 Update README 2026-04-24 07:12:40 +08:00
xiaji
65e3cd3513 Clear VM statuses on Proxmox shutdown 2026-04-17 18:53:14 +08:00
xiaji
f946f2e9a3 Show VM status, use separate threads for VM operations 2026-04-17 07:46:53 +08:00
xiaji
b1ea1249a9 Fix hang by using request_repaint in async thread 2026-04-17 07:43:20 +08:00
xiaji
c8e8db3412 Add per-VM action buttons in VM list 2026-04-17 07:38:34 +08:00
2 changed files with 139 additions and 160 deletions

View File

@@ -1,94 +1,50 @@
# Proxmox VM 控制工具 # Proxmox VM 控制
一个用于控制 Proxmox 虚拟机的工具集,包含 Python 任务控制器和 Rust GUI 应用程序。 一个用于控制 Proxmox 虚拟机的 Rust GUI 应用程序。
## 项目结构 ## 功能
- **Python 任务控制器** - `proxmox_task_controller.py` / `proxmox_task_runner.py` - **连接管理** - 配置 Proxmox API 地址、端口、令牌
- 待机监听:持续监控命令目录,平时保持低功耗状态 - **节点操作** - 刷新获取节点列表,关闭节点
- 自动启停:按需启动/关闭 Proxmox 虚拟机 - **虚拟机列表** - 显示所有虚拟机及运行状态
- 任务执行:通过 SSH 在 VM 内执行命令 - **虚拟机控制** - 每台虚拟机独立按钮:启动、停止、重启
- **实时日志** - 显示操作日志
- **Rust GUI 应用** - `src/` (基于 egui) ## 技术栈
- 图形界面:简单易用的 VM 控制面板
- API 调用:通过 Proxmox REST API 控制虚拟机
- 中文界面:支持中文字体显示
## 快速开始 - **GUI**: egui
- **HTTP**: reqwest + rustls-tls
- **异步**: tokio
- **构建**: MSYS2 MinGW
### Rust GUI 应用 ## 环境要求
#### 环境要求
- Rust 1.70+ - Rust 1.70+
- MSYS2 + MinGW (x86_64-pc-windows-gnu) - MSYS2 + MinGW (x86_64-pc-windows-gnu 工具链)
#### 编译 ## 编译
```bash ```bash
# Debug
cargo build --target x86_64-pc-windows-gnu
# Release
cargo build --release --target x86_64-pc-windows-gnu cargo build --release --target x86_64-pc-windows-gnu
``` ```
#### 配置 ## 配置
编辑 `.env` 文件 程序启动后在设置中填写
``` | 字段 | 说明 |
PROXMOX_HOST=your-proxmox-host |------|------|
PROXMOX_USER=root@pam | Host | Proxmox 主机地址 |
PROXMOX_TOKEN=your-api-token | 端口 | API 端口 (默认 8006) |
VM_ID=100 | 令牌ID | API 令牌 ID |
NODE=proxmox | 密钥 | API 令牌密钥 |
```
#### 运行 ## 运行
```bash ```bash
./target/x86_64-pc-windows-gnu/release/proxmox-vm-gui.exe ./target/x86_64-pc-windows-gnu/release/proxmox-vm-gui.exe
``` ```
### Python 任务控制器
#### 环境要求
- Python 3.8+
- Proxmox VE 6.x+
#### 安装
```bash
pip install -r proxmox_task/requirements.txt
```
#### 配置
```bash
export PROXMOX_HOST="your-proxmox-host"
export PROXMOX_USER="root@pam"
export PROXMOX_TOKEN="your-api-token"
export VM_ID="100"
```
#### 运行
```bash
python3 proxmox_task_controller.py
```
## 配置选项
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| PROXMOX_HOST | Proxmox 主机地址 | localhost |
| PROXMOX_USER | API 用户 | root@pam |
| PROXMOX_TOKEN | API Token | (必需) |
| VM_ID | 虚拟机 ID | 100 |
| NODE | 节点名称 | proxmox |
## 许可证 ## 许可证
MIT License MIT License

View File

@@ -89,7 +89,7 @@ pub struct AppState {
pub node: String, pub node: String,
pub nodes: Vec<String>, pub nodes: Vec<String>,
pub vms: Vec<(u32, String)>, pub vms: Vec<(u32, String)>,
pub vm_status: String, pub vm_statuses: std::collections::HashMap<u32, String>,
pub log_buffer: Vec<String>, pub log_buffer: Vec<String>,
pub is_connected: bool, pub is_connected: bool,
} }
@@ -118,7 +118,7 @@ impl AppState {
node: config.node.clone(), node: config.node.clone(),
nodes: Vec::new(), nodes: Vec::new(),
vms: Vec::new(), vms: Vec::new(),
vm_status: "未知".to_string(), vm_statuses: std::collections::HashMap::new(),
log_buffer: log, log_buffer: log,
is_connected: false, is_connected: false,
} }
@@ -299,6 +299,11 @@ impl eframe::App for App {
state.vms = vms.clone(); state.vms = vms.clone();
state.vm_id = vms[0].0; state.vm_id = vms[0].0;
state.add_log(&format!("✓ 找到 {} 台虚拟机", vms.len())); state.add_log(&format!("✓ 找到 {} 台虚拟机", vms.len()));
for (vid, _) in vms {
if let Ok(status) = c.get_vm_status(&node, vid).await {
state.vm_statuses.insert(vid, status);
}
}
} else { } else {
state.add_log("未找到虚拟机"); state.add_log("未找到虚拟机");
} }
@@ -316,82 +321,89 @@ impl eframe::App for App {
} }
}); });
// 虚拟机列表(只读 // 虚拟机列表(每行带操作按钮
ui.label("虚拟机列表:"); ui.label("虚拟机列表:");
egui::ScrollArea::vertical().max_height(120.0).stick_to_bottom(true).show(ui, |ui| { let vms_list = st.vms.clone();
let vm_count = st.vms.len(); let statuses = st.vm_statuses.clone();
ui.label(format!("{} 台虚拟机 (点击选择)", vm_count)); let client_ref = st.client.clone();
let node_ref = st.node.clone();
egui::ScrollArea::vertical().max_height(180.0).stick_to_bottom(true).show(ui, |ui| {
let vm_count = vms_list.len();
ui.label(format!("{} 台虚拟机", vm_count));
ui.separator(); ui.separator();
for (idx, (id, name)) in st.vms.iter().enumerate() { for (_, (id, name)) in vms_list.iter().enumerate() {
let is_selected = st.vm_id == *id; let status = statuses.get(id).cloned().unwrap_or_else(|| "未知".to_string());
let label = if is_selected { let status_color = if status == "running" { egui::Color32::GREEN } else { egui::Color32::GRAY };
format!("{} ({}) - 已选择", name, id)
} else {
format!("{} ({})", name, id)
};
if is_selected {
ui.colored_label(egui::Color32::LIGHT_BLUE, label);
} else {
ui.label(label);
}
}
});
// 选择VM后显示操作按钮
if !st.vms.is_empty() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("选中 VM 操作:"); ui.colored_label(status_color, format!(""));
let client = st.client.clone(); ui.label(format!("{} ({})", name, id));
let node = st.node.clone(); ui.colored_label(egui::Color32::LIGHT_BLUE, format!("[{}]", status));
let vm_id = st.vm_id; ui.add_space(10.0);
let state_clone = state.clone();
if ui.button("▶ 启动").clicked() { let client = client_ref.clone();
st.add_log(&format!("启动 VM {}...", vm_id)); let node = node_ref.clone();
ctx.request_repaint(); let vm_id = *id;
let state_clone = state.clone();
let ctx_clone = ctx.clone();
if ui.button("").clicked() {
state_clone.write().unwrap().add_log(&format!("启动 VM {}...", vm_id));
thread::spawn(move || { thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let client = client.lock().unwrap(); let client = client.lock().unwrap();
if let Some(c) = client.as_ref() { if let Some(c) = client.as_ref() {
match c.start_vm(&node, vm_id).await { match c.start_vm(&node, vm_id).await {
Ok(_) => state_clone.write().unwrap().add_log("✓ 启动成功"), Ok(_) => {
Err(e) => state_clone.write().unwrap().add_log(&format!("{}", e)), state_clone.write().unwrap().add_log("✓ 启动成功");
ctx_clone.request_repaint();
}
Err(e) => {
state_clone.write().unwrap().add_log(&format!("{}", e));
ctx_clone.request_repaint();
}
} }
} }
}); });
}); });
} }
let client2 = st.client.clone(); let client2 = client_ref.clone();
let node2 = st.node.clone(); let node2 = node_ref.clone();
let vm_id2 = st.vm_id; let vm_id2 = *id;
let state2 = state.clone(); let state2 = state.clone();
if ui.button("■ 停止").clicked() { let ctx2 = ctx.clone();
st.add_log(&format!("停止 VM {}...", vm_id2)); if ui.button("").clicked() {
ctx.request_repaint(); state2.write().unwrap().add_log(&format!("停止 VM {}...", vm_id2));
thread::spawn(move || { thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let client = client2.lock().unwrap(); let client = client2.lock().unwrap();
if let Some(c) = client.as_ref() { if let Some(c) = client.as_ref() {
match c.stop_vm(&node2, vm_id2).await { match c.stop_vm(&node2, vm_id2).await {
Ok(_) => state2.write().unwrap().add_log("✓ 停止成功"), Ok(_) => {
Err(e) => state2.write().unwrap().add_log(&format!("{}", e)), state2.write().unwrap().add_log("✓ 停止成功");
ctx2.request_repaint();
}
Err(e) => {
state2.write().unwrap().add_log(&format!("{}", e));
ctx2.request_repaint();
}
} }
} }
}); });
}); });
} }
let client3 = st.client.clone(); let client3 = client_ref.clone();
let node3 = st.node.clone(); let node3 = node_ref.clone();
let vm_id3 = st.vm_id; let vm_id3 = *id;
let state3 = state.clone(); let state3 = state.clone();
if ui.button("↻ 重启").clicked() { let ctx3 = ctx.clone();
st.add_log(&format!("重启 VM {}...", vm_id3)); if ui.button("").clicked() {
ctx.request_repaint(); state3.write().unwrap().add_log(&format!("重启 VM {}...", vm_id3));
thread::spawn(move || { thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@@ -401,11 +413,20 @@ impl eframe::App for App {
Ok(_) => { Ok(_) => {
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
match c.start_vm(&node3, vm_id3).await { match c.start_vm(&node3, vm_id3).await {
Ok(_) => state3.write().unwrap().add_log("✓ 重启成功"), Ok(_) => {
Err(e) => state3.write().unwrap().add_log(&format!("{}", e)), state3.write().unwrap().add_log("✓ 重启成功");
ctx3.request_repaint();
}
Err(e) => {
state3.write().unwrap().add_log(&format!("{}", e));
ctx3.request_repaint();
} }
} }
Err(e) => state3.write().unwrap().add_log(&format!("{}", e)), }
Err(e) => {
state3.write().unwrap().add_log(&format!("{}", e));
ctx3.request_repaint();
}
} }
} }
}); });
@@ -415,6 +436,7 @@ impl eframe::App for App {
} }
}); });
}); });
});
ui.add_space(12.0); ui.add_space(12.0);
@@ -534,6 +556,7 @@ impl eframe::App for App {
s.add_log("✓ 关机命令发送成功"); s.add_log("✓ 关机命令发送成功");
s.add_log(&format!("响应: {}", response)); s.add_log(&format!("响应: {}", response));
s.is_connected = false; s.is_connected = false;
s.vm_statuses.clear();
} }
Err(e) => { Err(e) => {
state.write().unwrap().add_log(&format!("✗ 关机失败: {}", e)); state.write().unwrap().add_log(&format!("✗ 关机失败: {}", e));