初始化项目:添加Proxmox GUI管理平台
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal 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
75
README.md
Normal 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
92
config_dialog.py
Normal 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
251
main_window.py
Normal 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
48
proxmox_gui.py
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PySide6>=6.5.0
|
||||||
|
proxmoxer>=1.3.0
|
||||||
195
styles.py
Normal file
195
styles.py
Normal 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
142
vm_manager.py
Normal 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
|
||||||
Reference in New Issue
Block a user