commit 140e014f810a857361d80340f639ecf097e753a6 Author: xiaji Date: Sun Jan 25 20:04:23 2026 +0800 初始化项目:添加Proxmox GUI管理平台 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a4e2f7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7823391 --- /dev/null +++ b/README.md @@ -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 diff --git a/config_dialog.py b/config_dialog.py new file mode 100644 index 0000000..e0216c4 --- /dev/null +++ b/config_dialog.py @@ -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() diff --git a/main_window.py b/main_window.py new file mode 100644 index 0000000..7d51a6b --- /dev/null +++ b/main_window.py @@ -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]}失败!") diff --git a/proxmox_gui.py b/proxmox_gui.py new file mode 100644 index 0000000..d1b2633 --- /dev/null +++ b/proxmox_gui.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..311dd48 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PySide6>=6.5.0 +proxmoxer>=1.3.0 diff --git a/styles.py b/styles.py new file mode 100644 index 0000000..e39c4d2 --- /dev/null +++ b/styles.py @@ -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; +} +""" diff --git a/vm_manager.py b/vm_manager.py new file mode 100644 index 0000000..267c06a --- /dev/null +++ b/vm_manager.py @@ -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