Compare commits
7 Commits
3173475a21
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e348dbc075 | ||
|
|
65e3cd3513 | ||
|
|
f946f2e9a3 | ||
|
|
b1ea1249a9 | ||
|
|
c8e8db3412 | ||
|
|
eb751fdb0b | ||
|
|
15334459be |
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
PROXMOX_HOST=proxmox.example.com
|
||||
PROXMOX_USER=root@pam
|
||||
PROXMOX_TOKEN=your-api-token-here
|
||||
VM_ID=100
|
||||
@@ -18,7 +18,8 @@ opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
# MinGW target config
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
rustflags = ["-C", "link-args=-static-libgcc"]
|
||||
94
README.md
94
README.md
@@ -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 虚拟机
|
||||
- 任务执行:通过 SSH 在 VM 内执行命令
|
||||
|
||||
- **Rust GUI 应用** - `src/` (基于 egui)
|
||||
- 图形界面:简单易用的 VM 控制面板
|
||||
- API 调用:通过 Proxmox REST API 控制虚拟机
|
||||
- 中文界面:支持中文字体显示
|
||||
- **连接管理** - 配置 Proxmox API 地址、端口、令牌
|
||||
- **节点操作** - 刷新获取节点列表,关闭节点
|
||||
- **虚拟机列表** - 显示所有虚拟机及运行状态
|
||||
- **虚拟机控制** - 每台虚拟机独立按钮:启动、停止、重启
|
||||
- **实时日志** - 显示操作日志
|
||||
|
||||
## 快速开始
|
||||
## 技术栈
|
||||
|
||||
### Rust GUI 应用
|
||||
- **GUI**: egui
|
||||
- **HTTP**: reqwest + rustls-tls
|
||||
- **异步**: tokio
|
||||
- **构建**: MSYS2 MinGW
|
||||
|
||||
#### 环境要求
|
||||
## 环境要求
|
||||
|
||||
- Rust 1.70+
|
||||
- MSYS2 + MinGW (x86_64-pc-windows-gnu)
|
||||
- MSYS2 + MinGW (x86_64-pc-windows-gnu 工具链)
|
||||
|
||||
#### 编译
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
# Debug
|
||||
cargo build --target x86_64-pc-windows-gnu
|
||||
|
||||
# Release
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
#### 配置
|
||||
## 配置
|
||||
|
||||
编辑 `.env` 文件:
|
||||
程序启动后在设置中填写:
|
||||
|
||||
```
|
||||
PROXMOX_HOST=your-proxmox-host
|
||||
PROXMOX_USER=root@pam
|
||||
PROXMOX_TOKEN=your-api-token
|
||||
VM_ID=100
|
||||
NODE=proxmox
|
||||
```
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| Host | Proxmox 主机地址 |
|
||||
| 端口 | API 端口 (默认 8006) |
|
||||
| 令牌ID | API 令牌 ID |
|
||||
| 密钥 | API 令牌密钥 |
|
||||
|
||||
#### 运行
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
./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
|
||||
66
microsoft-feedback.md
Normal file
66
microsoft-feedback.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Microsoft SmartScreen 反馈 / 申诉
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **文件名称**: proxmox-vm-gui.exe
|
||||
- **文件大小**: ~25 MB
|
||||
- **程序类型**: GUI 桌面应用程序 (Rust + egui)
|
||||
- **用途**: Proxmox 虚拟机管理工具
|
||||
- **官网**: https://www.proxmox.com/
|
||||
|
||||
## 程序描述
|
||||
|
||||
这是一个**自开发的 Proxmox 虚拟机管理工具**,用于通过 Proxmox VE API 控制虚拟机(启动、停止、重启、关机等)。
|
||||
|
||||
### 功能特性
|
||||
- 连接 Proxmox API 进行身份验证
|
||||
- 获取节点列表
|
||||
- 获取虚拟机列表
|
||||
- 启动/停止/重启虚拟机
|
||||
- 图形用户界面 (GUI)
|
||||
|
||||
### 技术栈
|
||||
- **编程语言**: Rust
|
||||
- **GUI 框架**: egui (https://egui.rs/)
|
||||
- **HTTP 客户端**: reqwest
|
||||
- **目标平台**: Windows x86_64
|
||||
|
||||
### 代码开源
|
||||
- 程序代码位于本地开发环境
|
||||
- 不包含任何恶意代码
|
||||
- 不连接未知服务器
|
||||
- 不收集用户数据
|
||||
|
||||
## 为什么被拦截?
|
||||
|
||||
SmartScreen 触发可能原因:
|
||||
1. **无数字签名** - 自签名代码未提交微软审核
|
||||
2. **新文件** - 首次分发给用户
|
||||
3. **非知名开发者** - 个人开发者项目
|
||||
|
||||
## 申诉请求
|
||||
|
||||
本人保证:
|
||||
1. 此程序为自主开发的合法工具
|
||||
2. 不包含病毒、木马、恶意代码
|
||||
3. 不收集用户敏感信息
|
||||
4. 代码仅用于管理自己的 Proxmox 虚拟机
|
||||
|
||||
**请求**:将此程序加入 SmartScreen 白名单,或指导如何完成代码签名流程。
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 开发者:[your email]
|
||||
- 项目地址:[如有]
|
||||
|
||||
---
|
||||
|
||||
## 如何提交申诉
|
||||
|
||||
1. 访问:https://aka.ms/wdsf (Windows Defender SmartScreen 反馈)
|
||||
2. 选择"文件被错误阻止"
|
||||
3. 上传 proxmox-vm-gui.exe
|
||||
4. 填写上述信息
|
||||
5. 提交
|
||||
|
||||
或者在 SmartScreen 警告界面点击"详细信息"链接提交反馈。
|
||||
14
sign.ps1
Normal file
14
sign.ps1
Normal file
@@ -0,0 +1,14 @@
|
||||
$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=ProxmoxVMGUI" -CertStoreLocation Cert:\CurrentUser\My
|
||||
Write-Host "Certificate Thumbprint: $($cert.Thumbprint)"
|
||||
|
||||
$pfxPath = "D:\selftools\proxmox-task\code_signing.pfx"
|
||||
|
||||
$securePassword = New-Object -TypeName System.Security.SecureString
|
||||
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
|
||||
$plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr)
|
||||
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
|
||||
|
||||
$pwd = ConvertTo-SecureString -String $plainPassword -AsPlainText -Force
|
||||
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pwd
|
||||
|
||||
Write-Host "Certificate exported to $pfxPath"
|
||||
18
src/api.rs
18
src/api.rs
@@ -253,30 +253,20 @@ impl ProxmoxClient {
|
||||
|
||||
pub async fn shutdown_node(&self, node: &str) -> Result<String, String> {
|
||||
let url = format!("{}/nodes/{}/status", self.base_url, node);
|
||||
println!("[API] 关机请求: POST {}", url);
|
||||
println!("[API] Headers: Authorization={}", self.auth_header());
|
||||
|
||||
let resp = self.client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&serde_json::json!({"action": "shutdown"}))
|
||||
.form(&[("command", "shutdown")])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let err = format!("网络错误: {}", e);
|
||||
println!("[API] {}", err);
|
||||
err
|
||||
})?;
|
||||
.map_err(|e| format!("网络错误: {}", e))?;
|
||||
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
println!("[API] 响应状态: {}", status);
|
||||
println!("[API] 响应内容: {}", body);
|
||||
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("HTTP {}: {}", status, body));
|
||||
}
|
||||
Ok(body)
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
}
|
||||
243
src/gui.rs
243
src/gui.rs
@@ -89,7 +89,7 @@ pub struct AppState {
|
||||
pub node: String,
|
||||
pub nodes: Vec<String>,
|
||||
pub vms: Vec<(u32, String)>,
|
||||
pub vm_status: String,
|
||||
pub vm_statuses: std::collections::HashMap<u32, String>,
|
||||
pub log_buffer: Vec<String>,
|
||||
pub is_connected: bool,
|
||||
}
|
||||
@@ -118,7 +118,7 @@ impl AppState {
|
||||
node: config.node.clone(),
|
||||
nodes: Vec::new(),
|
||||
vms: Vec::new(),
|
||||
vm_status: "未知".to_string(),
|
||||
vm_statuses: std::collections::HashMap::new(),
|
||||
log_buffer: log,
|
||||
is_connected: false,
|
||||
}
|
||||
@@ -145,7 +145,9 @@ pub fn gui_run() {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([520.0, 480.0])
|
||||
.with_min_inner_size([450.0, 400.0])
|
||||
.with_title("Proxmox VM 控制器"),
|
||||
.with_title("Proxmox VM 控制器")
|
||||
.with_decorations(true)
|
||||
.with_visible(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -274,103 +276,165 @@ impl eframe::App for App {
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
||||
// 虚拟机列表(只读)
|
||||
ui.label("虚拟机列表:");
|
||||
egui::ScrollArea::vertical().max_height(120.0).stick_to_bottom(true).show(ui, |ui| {
|
||||
let vm_count = st.vms.len();
|
||||
ui.label(format!("共 {} 台虚拟机 (点击选择)", vm_count));
|
||||
ui.separator();
|
||||
|
||||
for (idx, (id, name)) in st.vms.iter().enumerate() {
|
||||
let is_selected = st.vm_id == *id;
|
||||
let label = if is_selected {
|
||||
format!("● {} ({}) - 已选择", name, id)
|
||||
} else {
|
||||
format!("○ {} ({})", name, id)
|
||||
};
|
||||
if is_selected {
|
||||
ui.colored_label(egui::Color32::LIGHT_BLUE, label);
|
||||
// VM刷新按钮
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("🔄 刷新VM列表").clicked() {
|
||||
if st.nodes.is_empty() {
|
||||
st.add_log("请先获取节点列表");
|
||||
} else {
|
||||
ui.label(label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 选择VM后显示操作按钮
|
||||
if !st.vms.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("选中 VM 操作:");
|
||||
let client = st.client.clone();
|
||||
let node = st.node.clone();
|
||||
let vm_id = st.vm_id;
|
||||
let state_clone = state.clone();
|
||||
|
||||
if ui.button("▶ 启动").clicked() {
|
||||
st.add_log(&format!("启动 VM {}...", vm_id));
|
||||
let node = st.node.clone();
|
||||
let client = st.client.clone();
|
||||
let state_clone = state.clone();
|
||||
st.add_log(&format!("正在获取 {} 的虚拟机列表...", node));
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.start_vm(&node, vm_id).await {
|
||||
Ok(_) => state_clone.write().unwrap().add_log("✓ 启动成功"),
|
||||
Err(e) => state_clone.write().unwrap().add_log(&format!("✗ {}", e)),
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let client2 = st.client.clone();
|
||||
let node2 = st.node.clone();
|
||||
let vm_id2 = st.vm_id;
|
||||
let state2 = state.clone();
|
||||
if ui.button("■ 停止").clicked() {
|
||||
st.add_log(&format!("停止 VM {}...", vm_id2));
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client2.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.stop_vm(&node2, vm_id2).await {
|
||||
Ok(_) => state2.write().unwrap().add_log("✓ 停止成功"),
|
||||
Err(e) => state2.write().unwrap().add_log(&format!("✗ {}", e)),
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let client3 = st.client.clone();
|
||||
let node3 = st.node.clone();
|
||||
let vm_id3 = st.vm_id;
|
||||
let state3 = state.clone();
|
||||
if ui.button("↻ 重启").clicked() {
|
||||
st.add_log(&format!("重启 VM {}...", vm_id3));
|
||||
ctx.request_repaint();
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client3.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.stop_vm(&node3, vm_id3).await {
|
||||
Ok(_) => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
||||
match c.start_vm(&node3, vm_id3).await {
|
||||
Ok(_) => state3.write().unwrap().add_log("✓ 重启成功"),
|
||||
Err(e) => state3.write().unwrap().add_log(&format!("✗ {}", e)),
|
||||
match c.get_vms(&node).await {
|
||||
Ok(vms) => {
|
||||
let mut state = state_clone.write().unwrap();
|
||||
if !vms.is_empty() {
|
||||
state.vms = vms.clone();
|
||||
state.vm_id = vms[0].0;
|
||||
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 {
|
||||
state.add_log("未找到虚拟机");
|
||||
}
|
||||
}
|
||||
Err(e) => state3.write().unwrap().add_log(&format!("✗ {}", e)),
|
||||
Err(e) => {
|
||||
state_clone.write().unwrap().add_log(&format!("✗ 获取虚拟机失败: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state_clone.write().unwrap().add_log("未连接到服务器");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 虚拟机列表(每行带操作按钮)
|
||||
ui.label("虚拟机列表:");
|
||||
let vms_list = st.vms.clone();
|
||||
let statuses = st.vm_statuses.clone();
|
||||
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();
|
||||
|
||||
for (_, (id, name)) in vms_list.iter().enumerate() {
|
||||
let status = statuses.get(id).cloned().unwrap_or_else(|| "未知".to_string());
|
||||
let status_color = if status == "running" { egui::Color32::GREEN } else { egui::Color32::GRAY };
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(status_color, format!("●"));
|
||||
ui.label(format!("{} ({})", name, id));
|
||||
ui.colored_label(egui::Color32::LIGHT_BLUE, format!("[{}]", status));
|
||||
ui.add_space(10.0);
|
||||
|
||||
let client = client_ref.clone();
|
||||
let node = node_ref.clone();
|
||||
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 || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.start_vm(&node, vm_id).await {
|
||||
Ok(_) => {
|
||||
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 = client_ref.clone();
|
||||
let node2 = node_ref.clone();
|
||||
let vm_id2 = *id;
|
||||
let state2 = state.clone();
|
||||
let ctx2 = ctx.clone();
|
||||
if ui.button("■").clicked() {
|
||||
state2.write().unwrap().add_log(&format!("停止 VM {}...", vm_id2));
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client2.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.stop_vm(&node2, vm_id2).await {
|
||||
Ok(_) => {
|
||||
state2.write().unwrap().add_log("✓ 停止成功");
|
||||
ctx2.request_repaint();
|
||||
}
|
||||
Err(e) => {
|
||||
state2.write().unwrap().add_log(&format!("✗ {}", e));
|
||||
ctx2.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let client3 = client_ref.clone();
|
||||
let node3 = node_ref.clone();
|
||||
let vm_id3 = *id;
|
||||
let state3 = state.clone();
|
||||
let ctx3 = ctx.clone();
|
||||
if ui.button("↻").clicked() {
|
||||
state3.write().unwrap().add_log(&format!("重启 VM {}...", vm_id3));
|
||||
thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = client3.lock().unwrap();
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.stop_vm(&node3, vm_id3).await {
|
||||
Ok(_) => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
||||
match c.start_vm(&node3, vm_id3).await {
|
||||
Ok(_) => {
|
||||
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));
|
||||
ctx3.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -488,8 +552,11 @@ impl eframe::App for App {
|
||||
if let Some(c) = client.as_ref() {
|
||||
match c.shutdown_node(&node).await {
|
||||
Ok(response) => {
|
||||
state.write().unwrap().add_log("✓ 关机命令发送成功");
|
||||
state.write().unwrap().add_log(&format!("响应: {}", response));
|
||||
let mut s = state.write().unwrap();
|
||||
s.add_log("✓ 关机命令发送成功");
|
||||
s.add_log(&format!("响应: {}", response));
|
||||
s.is_connected = false;
|
||||
s.vm_statuses.clear();
|
||||
}
|
||||
Err(e) => {
|
||||
state.write().unwrap().add_log(&format!("✗ 关机失败: {}", e));
|
||||
|
||||
Reference in New Issue
Block a user