初始化项目:添加Proxmox GUI管理平台

This commit is contained in:
2026-01-25 20:04:23 +08:00
commit 140e014f81
8 changed files with 837 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
.trae/
.vscode/
.idea/
*.swp
*.swo
*~
config.json
*.log
.DS_Store
Thumbs.db

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# Proxmox GUI
企业级虚拟化管理平台基于PySide6开发的Proxmox VE图形化管理工具。
## 功能特点
- **服务器连接管理**
- 支持配置和连接多个Proxmox VE服务器
- 自动检测并安装依赖包
- **虚拟机管理**
- 实时显示虚拟机列表VMID、名称、状态、CPU、内存、磁盘、运行时间
- 支持虚拟机搜索和过滤
- 提供虚拟机统计概览(总数、运行中、已停止、已暂停)
- **操作控制**
- 启动虚拟机
- 关机(正常关机)
- 重启虚拟机
- 强制停止
- 挂起虚拟机
- **现代化界面**
- 暗色主题设计
- 响应式布局
- 直观的卡片式统计展示
- 操作按钮状态自动禁用(根据虚拟机状态)
## 系统要求
- Python 3.8+
- Windows/Linux/macOS
## 安装
1. 克隆仓库
```bash
git clone http://14.103.237.41:16001/xiaji/proxmox-gui.git
cd proxmox-gui
```
2. 安装依赖
```bash
pip install -r requirements.txt
```
## 使用
1. 运行程序
```bash
python proxmox_gui.py
```
2. 点击右上角"配置"按钮填写Proxmox服务器信息
- Host: 服务器地址
- User: 用户名
- Password: 密码
- Node: 节点名称默认pve
3. 点击"连接"按钮连接服务器
4. 在虚拟机列表中,可以通过操作按钮管理虚拟机
## 依赖项
- PySide6 >= 6.5.0
- proxmoxer >= 1.3.0
## 配置文件
配置信息保存在 `config.json` 文件中,包含服务器连接信息。
## 许可证
MIT License

92
config_dialog.py Normal file
View File

@@ -0,0 +1,92 @@
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QCheckBox, QPushButton, QHBoxLayout, QMessageBox)
from PySide6.QtCore import Qt
from vm_manager import VMManager
class ConfigDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.vm_manager = VMManager()
self.setup_ui()
self.load_current_config()
def setup_ui(self):
self.setWindowTitle("配置连接信息")
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.setSpacing(15)
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
self.host_edit = QLineEdit()
self.host_edit.setPlaceholderText("例如: 192.168.1.100")
form_layout.addRow("PVE 主机 IP:", self.host_edit)
self.user_edit = QLineEdit()
self.user_edit.setPlaceholderText("例如: root@pam")
form_layout.addRow("用户名:", self.user_edit)
self.password_edit = QLineEdit()
self.password_edit.setPlaceholderText("输入 PVE 密码")
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
form_layout.addRow("密码:", self.password_edit)
self.node_edit = QLineEdit()
self.node_edit.setPlaceholderText("例如: pve")
self.node_edit.setText("pve")
form_layout.addRow("节点名称:", self.node_edit)
self.ssl_checkbox = QCheckBox("验证 SSL 证书")
self.ssl_checkbox.setChecked(False)
form_layout.addRow("", self.ssl_checkbox)
layout.addLayout(form_layout)
layout.addSpacing(20)
button_layout = QHBoxLayout()
button_layout.addStretch()
self.save_button = QPushButton("保存配置")
self.save_button.clicked.connect(self.save_config)
button_layout.addWidget(self.save_button)
self.cancel_button = QPushButton("取消")
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
layout.addSpacing(10)
def load_current_config(self):
config = self.vm_manager.get_config()
self.host_edit.setText(config.get('host', ''))
self.user_edit.setText(config.get('user', ''))
self.password_edit.setText(config.get('password', ''))
self.node_edit.setText(config.get('node', 'pve'))
self.ssl_checkbox.setChecked(config.get('verify_ssl', False))
def save_config(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
node = self.node_edit.text().strip() or 'pve'
verify_ssl = self.ssl_checkbox.isChecked()
if not host or not user or not password:
QMessageBox.warning(self, "警告", "请填写完整的连接信息!")
return
config = {
'host': host,
'user': user,
'password': password,
'node': node,
'verify_ssl': verify_ssl
}
self.vm_manager.save_config(config)
QMessageBox.information(self, "成功", "配置已保存!")
self.accept()
def get_config(self):
return self.vm_manager.get_config()

251
main_window.py Normal file
View File

@@ -0,0 +1,251 @@
from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QLineEdit, QFrame, QMessageBox, QStatusBar, QDialog)
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QFont, QIcon, QAction
from vm_manager import VMManager
from config_dialog import ConfigDialog
from styles import DARK_STYLE, CARD_STYLE
class StatCard(QFrame):
def __init__(self, title: str, parent=None):
super().__init__(parent)
self.setObjectName("stat_card")
self.setStyleSheet(CARD_STYLE)
self.setMinimumSize(150, 100)
layout = QVBoxLayout(self)
layout.setSpacing(5)
layout.setContentsMargins(15, 15, 15, 15)
self.value_label = QLabel("0")
self.value_label.setObjectName("stat_value")
self.value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title_label = QLabel(title)
self.title_label.setObjectName("stat_title")
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.value_label)
layout.addWidget(self.title_label)
def set_value(self, value: str):
self.value_label.setText(str(value))
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.vm_manager = VMManager()
self.vms = []
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("企业级虚拟化管理平台")
self.setMinimumSize(1200, 800)
self.setStyleSheet(DARK_STYLE)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(0)
main_layout.setContentsMargins(20, 20, 20, 20)
title_layout = QHBoxLayout()
title_label = QLabel("企业级虚拟化管理平台")
title_label.setObjectName("title_label")
title_layout.addWidget(title_label)
title_layout.addStretch()
config_btn = QPushButton("配置")
config_btn.setObjectName("config_button")
config_btn.clicked.connect(self.open_config)
title_layout.addWidget(config_btn)
main_layout.addLayout(title_layout)
toolbar_layout = QHBoxLayout()
toolbar_layout.addStretch()
self.connect_btn = QPushButton("连接")
self.connect_btn.setObjectName("connect_button")
self.connect_btn.setFixedSize(120, 45)
self.connect_btn.clicked.connect(self.connect_and_load)
toolbar_layout.addWidget(self.connect_btn)
self.refresh_btn = QPushButton("刷新")
self.refresh_btn.setFixedSize(120, 45)
self.refresh_btn.clicked.connect(self.refresh_vms)
toolbar_layout.addWidget(self.refresh_btn)
main_layout.addLayout(toolbar_layout)
stats_layout = QHBoxLayout()
stats_layout.setSpacing(15)
self.total_card = StatCard("虚拟机总数")
stats_layout.addWidget(self.total_card)
self.running_card = StatCard("运行中")
stats_layout.addWidget(self.running_card)
self.stopped_card = StatCard("已停止")
stats_layout.addWidget(self.stopped_card)
self.paused_card = StatCard("已暂停")
stats_layout.addWidget(self.paused_card)
main_layout.addLayout(stats_layout)
list_section_layout = QVBoxLayout()
list_section_layout.setSpacing(10)
section_label = QLabel("虚拟机列表")
section_label.setObjectName("section_label")
list_section_layout.addWidget(section_label)
search_layout = QHBoxLayout()
self.search_edit = QLineEdit()
self.search_edit.setPlaceholderText("搜索虚拟机...")
self.search_edit.textChanged.connect(self.filter_vms)
search_layout.addWidget(self.search_edit)
list_section_layout.addLayout(search_layout)
main_layout.addLayout(list_section_layout)
self.table = QTableWidget()
self.table.setColumnCount(8)
self.table.verticalHeader().setDefaultSectionSize(100)
self.table.setHorizontalHeaderLabels(["VMID", "名称", "状态", "CPU", "内存", "磁盘", "运行时间", "操作"])
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
self.table.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.Interactive)
self.table.setColumnWidth(0, 60)
self.table.setColumnWidth(2, 80)
self.table.setColumnWidth(7, 480)
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.table.setAlternatingRowColors(True)
main_layout.addWidget(self.table)
self.setStatusBar(QStatusBar())
self.statusBar().setSizeGripEnabled(False)
def open_config(self):
dialog = ConfigDialog(self)
if dialog.exec() == QDialog.Accepted:
config = self.vm_manager.get_config()
self.statusBar().showMessage("配置已更新")
def connect_and_load(self):
config = self.vm_manager.get_config()
if not config.get('host') or not config.get('user') or not config.get('password'):
QMessageBox.warning(self, "警告", "请先配置连接信息!")
self.open_config()
return
self.connect_btn.setEnabled(False)
self.statusBar().showMessage("正在连接...")
if self.vm_manager.connect(
config['host'],
config['user'],
config['password'],
config.get('node', 'pve'),
config.get('verify_ssl', False)
):
self.load_vms()
self.statusBar().showMessage("连接成功")
else:
QMessageBox.critical(self, "错误", "连接失败,请检查配置!")
self.statusBar().showMessage("连接失败")
self.connect_btn.setEnabled(True)
def load_vms(self):
self.vms = self.vm_manager.get_vms()
stats = self.vm_manager.get_vm_stats(self.vms)
self.total_card.set_value(str(stats['total']))
self.running_card.set_value(str(stats['running']))
self.stopped_card.set_value(str(stats['stopped']))
self.paused_card.set_value(str(stats['paused']))
self.update_table(self.vms)
self.statusBar().showMessage(f"已加载 {len(self.vms)} 个虚拟机")
def refresh_vms(self):
if self.vm_manager.is_connected():
self.load_vms()
self.statusBar().showMessage("已刷新")
else:
QMessageBox.warning(self, "警告", "请先连接服务器!")
def filter_vms(self, text: str):
if not text:
self.update_table(self.vms)
else:
filtered = [vm for vm in self.vms
if text.lower() in str(vm.get('vmid', ''))
or text.lower() in vm.get('name', '').lower()]
self.update_table(filtered)
def update_table(self, vms: list):
self.table.setRowCount(0)
for vm in vms:
row = self.table.rowCount()
self.table.insertRow(row)
info = self.vm_manager.get_vm_info(vm)
status = info['status']
self.table.setItem(row, 0, QTableWidgetItem(str(info['vmid'])))
self.table.setItem(row, 1, QTableWidgetItem(info['name']))
status_item = QTableWidgetItem(info['status_text'])
status_item.setForeground(Qt.GlobalColor.green if status == 'running' else
Qt.GlobalColor.red if status == 'stopped' else
Qt.GlobalColor.yellow)
self.table.setItem(row, 2, status_item)
self.table.setItem(row, 3, QTableWidgetItem(info['cpu_percent']))
self.table.setItem(row, 4, QTableWidgetItem(info['memory']))
self.table.setItem(row, 5, QTableWidgetItem(info['disk']))
self.table.setItem(row, 6, QTableWidgetItem(info['uptime']))
action_widget = QWidget()
action_layout = QHBoxLayout(action_widget)
action_layout.setSpacing(5)
action_layout.setContentsMargins(5, 2, 5, 2)
node = self.vm_manager.get_config().get('node', 'pve')
vmid = info['vmid']
start_btn = self.create_action_button("启动", "action_start", vmid, node, 'start', status == 'running')
stop_btn = self.create_action_button("关机", "action_stop", vmid, node, 'shutdown', status != 'running')
restart_btn = self.create_action_button("重启", "action_restart", vmid, node, 'reboot', status != 'running')
force_stop_btn = self.create_action_button("强制停止", "action_stop", vmid, node, 'stop', status != 'running')
suspend_btn = self.create_action_button("挂起", "action_button", vmid, node, 'suspend', status != 'running')
action_layout.addWidget(start_btn)
action_layout.addWidget(stop_btn)
action_layout.addWidget(restart_btn)
action_layout.addWidget(force_stop_btn)
action_layout.addWidget(suspend_btn)
self.table.setCellWidget(row, 7, action_widget)
def create_action_button(self, text: str, obj_name: str, vmid: int, node: str, operation: str, disabled: bool):
btn = QPushButton(text)
btn.setObjectName(obj_name)
btn.setEnabled(not disabled)
btn.clicked.connect(lambda: self.execute_vm_operation(vmid, node, operation))
return btn
def execute_vm_operation(self, vmid: int, node: str, operation: str):
operation_names = {
'start': '启动',
'shutdown': '关机',
'reboot': '重启',
'stop': '强制停止',
'suspend': '挂起'
}
if self.vm_manager._vm_operation(vmid, node, operation):
QMessageBox.information(self, "成功", f"虚拟机 {vmid} {operation_names[operation]}命令已发送!")
self.load_vms()
else:
QMessageBox.critical(self, "错误", f"虚拟机 {vmid} {operation_names[operation]}失败!")

48
proxmox_gui.py Normal file
View File

@@ -0,0 +1,48 @@
import sys
import subprocess
import importlib
REQUIRED_PACKAGES = ['PySide6', 'proxmoxer']
def check_and_install_packages():
missing_packages = []
for package in REQUIRED_PACKAGES:
try:
importlib.import_module(package.replace('-', '_'))
except ImportError:
missing_packages.append(package)
if missing_packages:
print(f"缺少必要的第三方库: {', '.join(missing_packages)}")
print("正在尝试自动安装...")
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + missing_packages)
print("安装完成!")
return True
except subprocess.CalledProcessError as e:
print(f"自动安装失败: {e}")
print(f"请手动运行: pip install {' '.join(missing_packages)}")
return False
return True
def main():
if not check_and_install_packages():
sys.exit(1)
try:
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
app = QApplication(sys.argv)
app.setStyle('Fusion')
window = MainWindow()
window.show()
sys.exit(app.exec())
except Exception as e:
print(f"启动应用时发生错误: {e}")
sys.exit(1)
if __name__ == '__main__':
main()

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
PySide6>=6.5.0
proxmoxer>=1.3.0

195
styles.py Normal file
View File

@@ -0,0 +1,195 @@
from PySide6.QtCore import QFile, QTextStream
from PySide6.QtWidgets import QApplication
DARK_STYLE = """
QMainWindow {
background-color: #1e1e2e;
}
QWidget {
background-color: #1e1e2e;
color: #cdd6f4;
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
font-size: 14px;
}
QLabel#title_label {
font-size: 24px;
font-weight: bold;
color: #89b4fa;
}
QLabel#section_label {
font-size: 18px;
font-weight: bold;
color: #f5c2e7;
margin-top: 20px;
margin-bottom: 10px;
}
QPushButton {
background-color: #45475a;
color: #cdd6f4;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #585b70;
}
QPushButton:pressed {
background-color: #313244;
}
QPushButton#connect_button {
background-color: #89b4fa;
color: #1e1e2e;
font-size: 16px;
padding: 12px 30px;
}
QPushButton#connect_button:hover {
background-color: #b4befe;
}
QPushButton#config_button {
background-color: #f9e2af;
color: #1e1e2e;
}
QPushButton#config_button:hover {
background-color: #fbf8cc;
}
QPushButton#action_button,
QPushButton#action_start,
QPushButton#action_stop,
QPushButton#action_restart {
min-width: 70px;
padding: 6px 10px;
font-size: 12px;
}
QPushButton#action_start {
background-color: #a6e3a1;
color: #1e1e2e;
}
QPushButton#action_start:hover {
background-color: #94e2d5;
}
QPushButton#action_stop {
background-color: #f38ba8;
color: #1e1e2e;
}
QPushButton#action_stop:hover {
background-color: #eba0ac;
}
QPushButton#action_restart {
background-color: #fab387;
color: #1e1e2e;
}
QPushButton#action_restart:hover {
background-color: #f9c74f;
}
QLineEdit {
background-color: #313244;
color: #cdd6f4;
border: 2px solid #45475a;
border-radius: 8px;
padding: 10px;
selection-background-color: #585b70;
}
QLineEdit:focus {
border-color: #89b4fa;
}
QLineEdit:placeholder-text {
color: #6c7086;
}
QTableWidget {
background-color: #313244;
color: #cdd6f4;
border: none;
border-radius: 12px;
gridline-color: #45475a;
selection-background-color: #45475a;
}
QHeaderView::section {
background-color: #45475a;
color: #cdd6f4;
padding: 12px;
font-weight: bold;
border: none;
}
QTableWidget::item {
padding: 12px;
border-bottom: 1px solid #45475a;
}
QTableWidget::item:selected {
background-color: #45475a;
}
QStatusBar {
background-color: #313244;
color: #a6adc8;
padding: 8px;
}
QMessageBox {
background-color: #1e1e2e;
}
QDialog {
background-color: #1e1e2e;
}
QGroupBox {
border: 2px solid #45475a;
border-radius: 12px;
margin-top: 10px;
font-weight: bold;
color: #f5c2e7;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 10px;
color: #f5c2e7;
}
"""
CARD_STYLE = """
QFrame#stat_card {
background-color: #313244;
border-radius: 12px;
padding: 16px;
border: 1px solid #45475a;
}
QLabel#stat_value {
font-size: 28px;
font-weight: bold;
color: #89b4fa;
}
QLabel#stat_title {
font-size: 14px;
color: #a6adc8;
margin-top: 4px;
}
"""

142
vm_manager.py Normal file
View File

@@ -0,0 +1,142 @@
from proxmoxer import ProxmoxAPI
from typing import List, Dict, Any, Optional
import json
import os
from datetime import datetime
CONFIG_FILE = "config.json"
class VMManager:
def __init__(self):
self.proxmox: Optional[ProxmoxAPI] = None
self.config: Dict[str, Any] = {}
self.load_config()
def load_config(self) -> Dict[str, Any]:
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
self.config = json.load(f)
except Exception:
self.config = {}
return self.config
def save_config(self, config: Dict[str, Any]) -> None:
self.config = config
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
def get_config(self) -> Dict[str, Any]:
return self.config
def connect(self, host: str, user: str, password: str,
node: str = "pve", verify_ssl: bool = False) -> bool:
try:
self.proxmox = ProxmoxAPI(
host,
user=user,
password=password,
verify_ssl=verify_ssl
)
self.proxmox.version.get()
return True
except Exception as e:
print(f"连接失败: {e}")
return False
def is_connected(self) -> bool:
return self.proxmox is not None
def get_vms(self) -> List[Dict[str, Any]]:
if not self.proxmox:
return []
try:
resources = self.proxmox.cluster.resources.get()
vms = [r for r in resources if r['type'] == 'qemu']
return vms
except Exception as e:
print(f"获取虚拟机列表失败: {e}")
return []
def get_vm_stats(self, vms: List[Dict]) -> Dict[str, int]:
stats = {"total": len(vms), "running": 0, "stopped": 0, "paused": 0}
for vm in vms:
status = vm.get('status', '').lower()
if status == 'running':
stats['running'] += 1
elif status == 'stopped':
stats['stopped'] += 1
elif status == 'paused':
stats['paused'] += 1
return stats
def format_memory(self, bytes_value: int) -> str:
gb = bytes_value / (1024 ** 3)
return f"{gb:.1f} GB"
def format_uptime(self, seconds: int) -> str:
if seconds <= 0:
return "-"
days = seconds // 86400
hours = (seconds % 86400) // 3600
if days > 0:
return f"{days}d {hours}h"
return f"{hours}h"
def get_vm_info(self, vm: Dict) -> Dict[str, Any]:
status = vm.get('status', 'unknown').lower()
status_text = '运行中' if status == 'running' else '已停止' if status == 'stopped' else '已暂停' if status == 'paused' else status
maxcpu = vm.get('maxcpu', 0)
cpu = vm.get('cpu', 0)
cpu_percent = f"{int(cpu * 100)}%" if maxcpu > 0 else "0%"
mem = vm.get('mem', 0)
maxmem = vm.get('maxmem', 0)
memory_text = f"{self.format_memory(mem)}/{self.format_memory(maxmem)}" if maxmem > 0 else "-"
disk = vm.get('disk', 0)
maxdisk = vm.get('maxdisk', 0)
disk_text = f"{self.format_memory(disk)}/{self.format_memory(maxdisk)}" if maxdisk > 0 else "-"
uptime_seconds = vm.get('uptime', 0)
uptime_text = self.format_uptime(uptime_seconds)
return {
'vmid': vm.get('vmid', 0),
'name': vm.get('name', '无名称'),
'status': status,
'status_text': status_text,
'cpu_percent': cpu_percent,
'memory': memory_text,
'disk': disk_text,
'uptime': uptime_text
}
def start_vm(self, vmid: int, node: str) -> bool:
return self._vm_operation(vmid, node, 'start')
def shutdown_vm(self, vmid: int, node: str) -> bool:
return self._vm_operation(vmid, node, 'shutdown')
def reboot_vm(self, vmid: int, node: str) -> bool:
return self._vm_operation(vmid, node, 'reboot')
def stop_vm(self, vmid: int, node: str) -> bool:
return self._vm_operation(vmid, node, 'stop')
def suspend_vm(self, vmid: int, node: str) -> bool:
return self._vm_operation(vmid, node, 'suspend')
def _vm_operation(self, vmid: int, node: str, operation: str) -> bool:
if not self.proxmox:
return False
try:
endpoint = self.proxmox.nodes(node).qemu(vmid).status
if hasattr(endpoint, operation):
getattr(endpoint, operation).post()
return True
return False
except Exception as e:
print(f"操作失败 ({operation}): {e}")
return False