# mainwindow.py import sys import logging import re import os # 导入 os 模块 from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem, QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog) from PySide6.QtCore import Qt, QPoint # 导入自动生成的 UI 文件 from ui_form import Ui_MainWindow # 导入我们自己编写的系统信息管理模块 from system_info import SystemInfoManager # 导入日志配置 from logger_config import setup_logging, logger # 导入磁盘操作模块 from disk_operations import DiskOperations # 导入 RAID 操作模块 from raid_operations import RaidOperations # 导入 LVM 操作模块 from lvm_operations import LvmOperations # 导入自定义对话框 from dialogs import (CreatePartitionDialog, MountDialog, CreateRaidDialog, CreatePvDialog, CreateVgDialog, CreateLvDialog) class MainWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) setup_logging(self.ui.logOutputTextEdit) logger.info("应用程序启动。") # 初始化管理器和操作类 self.system_manager = SystemInfoManager() self.disk_ops = DiskOperations() self.raid_ops = RaidOperations() self.lvm_ops = LvmOperations() # 连接刷新按钮的信号到槽函数 if hasattr(self.ui, 'refreshButton'): self.ui.refreshButton.clicked.connect(self.refresh_all_info) else: logger.warning("Warning: refreshButton not found in UI. Please add it in form.ui and regenerate ui_form.py.") # 启用 treeWidget 的自定义上下文菜单 self.ui.treeWidget_block_devices.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.treeWidget_block_devices.customContextMenuRequested.connect(self.show_block_device_context_menu) self.ui.treeWidget_raid.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.treeWidget_raid.customContextMenuRequested.connect(self.show_raid_context_menu) self.ui.treeWidget_lvm.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu) # 初始化时刷新所有数据 self.refresh_all_info() logger.info("所有设备信息已初始化加载。") def refresh_all_info(self): """ 刷新所有设备信息:块设备、RAID和LVM。 """ logger.info("开始刷新所有设备信息...") self.refresh_block_devices_info() self.refresh_raid_info() self.refresh_lvm_info() logger.info("所有设备信息刷新完成。") # --- 块设备概览 Tab --- def refresh_block_devices_info(self): self.ui.treeWidget_block_devices.clear() columns = [ ("设备名", 'name'), ("类型", 'type'), ("大小", 'size'), ("挂载点", 'mountpoint'), ("文件系统", 'fstype'), ("只读", 'ro'), ("UUID", 'uuid'), ("PARTUUID", 'partuuid'), ("厂商", 'vendor'), ("型号", 'model'), ("序列号", 'serial'), ("主次号", 'maj:min'), ("父设备名", 'pkname'), ] headers = [col[0] for col in columns] self.field_keys = [col[1] for col in columns] self.ui.treeWidget_block_devices.setColumnCount(len(headers)) self.ui.treeWidget_block_devices.setHeaderLabels(headers) for i in range(len(headers)): self.ui.treeWidget_block_devices.header().setSectionResizeMode(i, QHeaderView.ResizeToContents) try: devices = self.system_manager.get_block_devices() for dev in devices: self._add_device_to_tree(self.ui.treeWidget_block_devices, dev) for i in range(len(headers)): self.ui.treeWidget_block_devices.resizeColumnToContents(i) logger.info("块设备信息刷新成功。") except Exception as e: QMessageBox.critical(self, "错误", f"刷新块设备信息失败: {e}") logger.error(f"刷新块设备信息失败: {e}") def _add_device_to_tree(self, parent_item, dev_data): item = QTreeWidgetItem(parent_item) for i, key in enumerate(self.field_keys): value = dev_data.get(key) if key == 'ro': item.setText(i, "是" if value else "否") elif value is None: item.setText(i, "") else: item.setText(i, str(value)) item.setData(0, Qt.UserRole, dev_data) if 'children' in dev_data: for child in dev_data['children']: self._add_device_to_tree(item, child) item.setExpanded(True) def show_block_device_context_menu(self, pos: QPoint): item = self.ui.treeWidget_block_devices.itemAt(pos) menu = QMenu(self) # 菜单项:创建 RAID 阵列,创建 PV create_menu = QMenu("创建...", self) create_raid_action = create_menu.addAction("创建 RAID 阵列...") create_raid_action.triggered.connect(self._handle_create_raid_array) create_pv_action = create_menu.addAction("创建物理卷 (PV)...") create_pv_action.triggered.connect(self._handle_create_pv) menu.addMenu(create_menu) menu.addSeparator() if item: dev_data = item.data(0, Qt.UserRole) if not dev_data: logger.warning(f"无法获取设备 {item.text(0)} 的详细数据。") return device_name = dev_data.get('name') device_type = dev_data.get('type') mount_point = dev_data.get('mountpoint') device_path = dev_data.get('path') # 使用lsblk提供的完整路径 if not device_path: # 如果path字段缺失,则尝试从name构造 device_path = f"/dev/{device_name}" # 针对磁盘 (disk) 的操作 if device_type == 'disk': create_partition_action = menu.addAction(f"创建分区 {device_path}...") create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data.get('size'))) # 传递原始大小字符串 menu.addSeparator() # 挂载/卸载操作 (针对分区 'part' 和 RAID/LVM 逻辑卷,但这里只处理 'part') if device_type == 'part': if not mount_point or mount_point == '' or mount_point == 'N/A': mount_action = menu.addAction(f"挂载 {device_path}...") mount_action.triggered.connect(lambda: self._handle_mount(device_path)) elif mount_point != '[SWAP]': unmount_action = menu.addAction(f"卸载 {device_path}") # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_action.triggered.connect(lambda: self._unmount_and_refresh(device_path)) menu.addSeparator() # 删除分区和格式化操作 (针对分区 'part') delete_action = menu.addAction(f"删除分区 {device_path}") delete_action.triggered.connect(lambda: self._handle_delete_partition(device_path)) format_action = menu.addAction(f"格式化分区 {device_path}...") format_action.triggered.connect(lambda: self._handle_format_partition(device_path)) if menu.actions(): menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos)) else: logger.info("右键点击了空白区域或设备没有可用的操作。") def _handle_create_partition(self, disk_path, total_size_str): # 接收原始大小字符串 # 1. 解析磁盘总大小 (MiB) total_disk_mib = 0.0 logger.debug(f"尝试为磁盘 {disk_path} 创建分区。原始大小字符串: '{total_size_str}'") if total_size_str: match = re.match(r'(\d+(\.\d+)?)\s*([KMGT]?B?)', total_size_str, re.IGNORECASE) if match: value = float(match.group(1)) unit = match.group(3).upper() if match.group(3) else '' if unit == 'KB' or unit == 'K': total_disk_mib = value / 1024 elif unit == 'MB' or unit == 'M': total_disk_mib = value elif unit == 'GB' or unit == 'G': total_disk_mib = value * 1024 elif unit == 'TB' or unit == 'T': total_disk_mib = value * 1024 * 1024 elif unit == 'B': total_disk_mib = value / (1024 * 1024) else: logger.warning(f"无法识别磁盘 {disk_path} 的大小单位: '{unit}' (原始: '{total_size_str}')") total_disk_mib = 0.0 logger.debug(f"解析后的磁盘总大小 (MiB): {total_disk_mib}") else: logger.warning(f"无法解析磁盘 {disk_path} 的大小字符串 '{total_size_str}'。正则表达式不匹配。") total_disk_mib = 0.0 else: logger.warning(f"获取磁盘 {disk_path} 的大小字符串为空或None。") total_disk_mib = 0.0 if total_disk_mib <= 0.0: QMessageBox.critical(self, "错误", f"无法获取磁盘 {disk_path} 的有效总大小。") return # 2. 获取下一个分区的起始位置 (MiB) start_position_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path) if start_position_mib is None: # 如果获取起始位置失败 QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") return # 3. 计算最大可用空间 (MiB) max_available_mib = total_disk_mib - start_position_mib if max_available_mib < 0: # 安全检查,理论上不应该发生 max_available_mib = 0.0 # 确保 max_available_mib 至少为 1 MiB,以避免 spinbox 最小值大于最大值的问题 # 0.1 GB = 102.4 MiB, so 1 MiB is a safe minimum for fdisk/parted if max_available_mib < 1.0: max_available_mib = 1.0 # 最小可分配空间 (1 MiB) dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib) # 传递 total_disk_mib 和 max_available_mib if dialog.exec() == QDialog.Accepted: info = dialog.get_partition_info() if info: # Check if info is not None (dialog might have returned None on validation error) # 调用 disk_ops.create_partition,传递 total_disk_mib 和 use_max_space 标志 new_partition_path = self.disk_ops.create_partition( info['disk_path'], info['partition_table_type'], info['size_gb'], info['total_disk_mib'], # 传递磁盘总大小 (MiB) info['use_max_space'] # 传递是否使用最大空间的标志 ) if new_partition_path: self.refresh_all_info() # 刷新以显示新创建的分区 # 询问用户是否要格式化新创建的分区 reply = QMessageBox.question(None, "格式化新分区", f"分区 {new_partition_path} 已成功创建。\n" "您现在想格式化这个分区吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.Yes: # 调用格式化函数,fstype=None 会弹出选择框 if self.disk_ops.format_partition(new_partition_path, fstype=None): self.refresh_all_info() # 格式化成功后再次刷新 # else: create_partition already shows an error message # else: dialog was cancelled, no action needed def _handle_mount(self, device_path): dialog = MountDialog(self, device_path) if dialog.exec() == QDialog.Accepted: info = dialog.get_mount_info() if info: # 确保 info 不是 None (用户可能在 MountDialog 中取消了操作) if self.disk_ops.mount_partition(device_path, info['mount_point'], info['add_to_fstab']): # <--- 传递 add_to_fstab self.refresh_all_info() # 新增辅助方法,用于卸载并刷新 def _unmount_and_refresh(self, device_path): """Helper method to unmount a device and then refresh all info.""" if self.disk_ops.unmount_partition(device_path, show_dialog_on_error=True): self.refresh_all_info() def _handle_delete_partition(self, device_path): # delete_partition 内部会调用 unmount_partition 和 _remove_fstab_entry if self.disk_ops.delete_partition(device_path): self.refresh_all_info() def _handle_format_partition(self, device_path): # format_partition 内部会调用 unmount_partition 和 _remove_fstab_entry if self.disk_ops.format_partition(device_path): self.refresh_all_info() # --- RAID 管理 Tab --- def refresh_raid_info(self): self.ui.treeWidget_raid.clear() raid_headers = [ "阵列设备", "级别", "状态", "大小", "活动设备", "失败设备", "备用设备", "总设备数", "UUID", "名称", "Chunk Size", "挂载点" # 添加挂载点列 ] self.ui.treeWidget_raid.setColumnCount(len(raid_headers)) self.ui.treeWidget_raid.setHeaderLabels(raid_headers) for i in range(len(raid_headers)): self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents) try: raid_arrays = self.system_manager.get_mdadm_arrays() if not raid_arrays: item = QTreeWidgetItem(self.ui.treeWidget_raid) item.setText(0, "未找到RAID阵列。") logger.info("未找到RAID阵列。") return for array in raid_arrays: array_item = QTreeWidgetItem(self.ui.treeWidget_raid) array_path = array.get('device', 'N/A') # 确保 array_path 是一个有效的路径,否则上下文菜单会失效 if not array_path or array_path == 'N/A': logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。") continue current_mount_point = self.system_manager.get_mountpoint_for_device(array_path) array_item.setText(0, array_path) array_item.setText(1, array.get('level', 'N/A')) array_item.setText(2, array.get('state', 'N/A')) array_item.setText(3, array.get('array_size', 'N/A')) array_item.setText(4, array.get('active_devices', 'N/A')) array_item.setText(5, array.get('failed_devices', 'N/A')) array_item.setText(6, array.get('spare_devices', 'N/A')) array_item.setText(7, array.get('total_devices', 'N/A')) array_item.setText(8, array.get('uuid', 'N/A')) array_item.setText(9, array.get('name', 'N/A')) array_item.setText(10, array.get('chunk_size', 'N/A')) array_item.setText(11, current_mount_point if current_mount_point else "") # 显示挂载点 array_item.setExpanded(True) # 存储原始数据,用于上下文菜单 # 确保 array_path 字段在存储的数据中是正确的 array_data_for_context = array.copy() array_data_for_context['device'] = array_path array_item.setData(0, Qt.UserRole, array_data_for_context) # 添加成员设备作为子节点 for member in array.get('member_devices', []): member_item = QTreeWidgetItem(array_item) member_item.setText(0, f" {member.get('device_path', 'N/A')}") member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") member_item.setText(2, member.get('state', 'N/A')) member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}") # 成员设备不存储UserRole数据,因为操作通常针对整个阵列 # 自动调整列宽 for i in range(len(raid_headers)): self.ui.treeWidget_raid.resizeColumnToContents(i) logger.info("RAID阵列信息刷新成功。") except Exception as e: QMessageBox.critical(self, "错误", f"刷新RAID阵列信息失败: {e}") logger.error(f"刷新RAID阵列信息失败: {e}") def show_raid_context_menu(self, pos: QPoint): item = self.ui.treeWidget_raid.itemAt(pos) menu = QMenu(self) # 始终提供创建 RAID 阵列的选项 create_raid_action = menu.addAction("创建 RAID 阵列...") create_raid_action.triggered.connect(self._handle_create_raid_array) menu.addSeparator() if item and item.parent() is None: # 只有点击的是顶层RAID阵列项 array_data = item.data(0, Qt.UserRole) if not array_data: logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。") return array_path = array_data.get('device') # e.g., /dev/md126 member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])] if not array_path or array_path == 'N/A': logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。") return # 如果路径无效,则不显示任何操作 # Check if RAID array is mounted current_mount_point = self.system_manager.get_mountpoint_for_device(array_path) if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '': unmount_action = menu.addAction(f"卸载 {array_path} ({current_mount_point})") # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_action.triggered.connect(lambda: self._unmount_and_refresh(array_path)) else: mount_action = menu.addAction(f"挂载 {array_path}...") mount_action.triggered.connect(lambda: self._handle_mount(array_path)) menu.addSeparator() stop_action = menu.addAction(f"停止阵列 {array_path}") stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path)) delete_action = menu.addAction(f"删除阵列 {array_path}") delete_action.triggered.connect(lambda: self._handle_delete_raid_array(array_path, member_devices)) format_action = menu.addAction(f"格式化阵列 {array_path}...") # RAID 阵列的格式化也直接调用 disk_ops.format_partition format_action.triggered.connect(lambda: self._handle_format_raid_array(array_path)) if menu.actions(): menu.exec(self.ui.treeWidget_raid.mapToGlobal(pos)) else: logger.info("右键点击了空白区域或没有可用的RAID操作。") def _handle_create_raid_array(self): # 获取所有可用于创建RAID阵列的设备 # get_unallocated_partitions 现在应该返回所有合适的磁盘和分区 available_devices = self.system_manager.get_unallocated_partitions() if not available_devices: QMessageBox.warning(self, "警告", "没有可用于创建 RAID 阵列的设备。请确保有未挂载、未被LVM或RAID使用的磁盘或分区。") return dialog = CreateRaidDialog(self, available_devices) if dialog.exec() == QDialog.Accepted: info = dialog.get_raid_info() if info: # Check if info is not None if self.raid_ops.create_raid_array(info['devices'], info['level'], info['chunk_size']): self.refresh_all_info() def _handle_stop_raid_array(self, array_path): if self.raid_ops.stop_raid_array(array_path): self.refresh_all_info() def _handle_delete_raid_array(self, array_path, member_devices): if self.raid_ops.delete_raid_array(array_path, member_devices): self.refresh_all_info() def _handle_format_raid_array(self, array_path): # 格式化RAID阵列与格式化分区类似,可以复用DiskOperations中的逻辑 # 或者在RaidOperations中实现一个独立的format方法 # 这里我们直接调用DiskOperations的format_partition方法 # format_partition 内部会调用 unmount_partition(..., show_dialog_on_error=False) 和 _remove_fstab_entry if self.disk_ops.format_partition(array_path): self.refresh_all_info() # --- LVM 管理 Tab --- def refresh_lvm_info(self): self.ui.treeWidget_lvm.clear() lvm_headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"] # 添加挂载点列 self.ui.treeWidget_lvm.setColumnCount(len(lvm_headers)) self.ui.treeWidget_lvm.setHeaderLabels(lvm_headers) for i in range(len(lvm_headers)): self.ui.treeWidget_lvm.header().setSectionResizeMode(i, QHeaderView.ResizeToContents) try: lvm_data = self.system_manager.get_lvm_info() if not lvm_data.get('pvs') and not lvm_data.get('vgs') and not lvm_data.get('lvs'): item = QTreeWidgetItem(self.ui.treeWidget_lvm) item.setText(0, "未找到LVM信息。") logger.info("未找到LVM信息。") return # 物理卷 (PVs) pv_root_item = QTreeWidgetItem(self.ui.treeWidget_lvm) pv_root_item.setText(0, "物理卷 (PVs)") pv_root_item.setExpanded(True) pv_root_item.setData(0, Qt.UserRole, {'type': 'pv_root'}) # 标识根节点 if lvm_data.get('pvs'): for pv in lvm_data['pvs']: pv_item = QTreeWidgetItem(pv_root_item) pv_name = pv.get('pv_name', 'N/A') if pv_name.startswith('/dev/'): # Ensure it's a full path pv_path = pv_name else: # Attempt to construct if it's just a name, though pv_name from pvs usually is full path # Fallback, may not be accurate if system_manager.get_lvm_info() isn't consistent pv_path = f"/dev/{pv_name}" if pv_name != 'N/A' else 'N/A' pv_item.setText(0, pv_name) pv_item.setText(1, pv.get('pv_size', 'N/A')) pv_item.setText(2, pv.get('pv_attr', 'N/A')) pv_item.setText(3, pv.get('pv_uuid', 'N/A')) pv_item.setText(4, f"VG: {pv.get('vg_name', 'N/A')}") pv_item.setText(5, f"空闲: {pv.get('pv_free', 'N/A')}") pv_item.setText(6, pv.get('pv_fmt', 'N/A')) pv_item.setText(7, "") # PV没有直接挂载点 # 存储原始数据,确保pv_name是正确的设备路径 pv_data_for_context = pv.copy() pv_data_for_context['pv_name'] = pv_path # Store the full path for context menu pv_item.setData(0, Qt.UserRole, {'type': 'pv', 'data': pv_data_for_context}) # 存储原始数据 else: item = QTreeWidgetItem(pv_root_item) item.setText(0, "未找到物理卷。") # 卷组 (VGs) vg_root_item = QTreeWidgetItem(self.ui.treeWidget_lvm) vg_root_item.setText(0, "卷组 (VGs)") vg_root_item.setExpanded(True) vg_root_item.setData(0, Qt.UserRole, {'type': 'vg_root'}) if lvm_data.get('vgs'): for vg in lvm_data['vgs']: vg_item = QTreeWidgetItem(vg_root_item) vg_name = vg.get('vg_name', 'N/A') vg_item.setText(0, vg_name) vg_item.setText(1, vg.get('vg_size', 'N/A')) vg_item.setText(2, vg.get('vg_attr', 'N/A')) vg_item.setText(3, vg.get('vg_uuid', 'N/A')) vg_item.setText(4, f"PVs: {vg.get('pv_count', 'N/A')}, LVs: {vg.get('lv_count', 'N/A')}") vg_item.setText(5, f"空闲: {vg.get('vg_free', 'N/A')}, 已分配: {vg.get('vg_alloc_percent', 'N/A')}%") vg_item.setText(6, vg.get('vg_fmt', 'N/A')) vg_item.setText(7, "") # VG没有直接挂载点 # 存储原始数据,确保vg_name是正确的 vg_data_for_context = vg.copy() vg_data_for_context['vg_name'] = vg_name vg_item.setData(0, Qt.UserRole, {'type': 'vg', 'data': vg_data_for_context}) else: item = QTreeWidgetItem(vg_root_item) item.setText(0, "未找到卷组。") # 逻辑卷 (LVs) lv_root_item = QTreeWidgetItem(self.ui.treeWidget_lvm) lv_root_item.setText(0, "逻辑卷 (LVs)") lv_root_item.setExpanded(True) lv_root_item.setData(0, Qt.UserRole, {'type': 'lv_root'}) if lvm_data.get('lvs'): for lv in lvm_data['lvs']: lv_item = QTreeWidgetItem(lv_root_item) lv_name = lv.get('lv_name', 'N/A') vg_name = lv.get('vg_name', 'N/A') lv_attr = lv.get('lv_attr', '') # 确保 lv_path 是一个有效的路径 lv_path = lv.get('lv_path') if not lv_path or lv_path == 'N/A': # 如果 lv_path 不存在或为 N/A,则尝试从 vg_name 和 lv_name 构造 if vg_name != 'N/A' and lv_name != 'N/A': lv_path = f"/dev/{vg_name}/{lv_name}" else: lv_path = 'N/A' # 实在无法构造则标记为N/A current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path) lv_item.setText(0, lv_name) lv_item.setText(1, lv.get('lv_size', 'N/A')) lv_item.setText(2, lv_attr) lv_item.setText(3, lv.get('lv_uuid', 'N/A')) lv_item.setText(4, f"VG: {vg_name}, Origin: {lv.get('origin', 'N/A')}") lv_item.setText(5, f"快照: {lv.get('snap_percent', 'N/A')}%") lv_item.setText(6, lv_path) # 显示构造或获取的路径 lv_item.setText(7, current_mount_point if current_mount_point else "") # 显示挂载点 # 存储原始数据,确保 lv_path, lv_name, vg_name, lv_attr 都是正确的 lv_data_for_context = lv.copy() lv_data_for_context['lv_path'] = lv_path lv_data_for_context['lv_name'] = lv_name lv_data_for_context['vg_name'] = vg_name lv_data_for_context['lv_attr'] = lv_attr lv_item.setData(0, Qt.UserRole, {'type': 'lv', 'data': lv_data_for_context}) else: item = QTreeWidgetItem(lv_root_item) item.setText(0, "未找到逻辑卷。") for i in range(len(lvm_headers)): self.ui.treeWidget_lvm.resizeColumnToContents(i) logger.info("LVM信息刷新成功。") except Exception as e: QMessageBox.critical(self, "错误", f"刷新LVM信息失败: {e}") logger.error(f"刷新LVM信息失败: {e}") def show_lvm_context_menu(self, pos: QPoint): item = self.ui.treeWidget_lvm.itemAt(pos) menu = QMenu(self) # 根节点或空白区域的创建菜单 create_menu = QMenu("创建...", self) create_pv_action = create_menu.addAction("创建物理卷 (PV)...") create_pv_action.triggered.connect(self._handle_create_pv) create_vg_action = create_menu.addAction("创建卷组 (VG)...") create_vg_action.triggered.connect(self._handle_create_vg) create_lv_action = create_menu.addAction("创建逻辑卷 (LV)...") create_lv_action.triggered.connect(self._handle_create_lv) menu.addMenu(create_menu) menu.addSeparator() if item: item_data = item.data(0, Qt.UserRole) if not item_data: logger.warning(f"无法获取 LVM 项 {item.text(0)} 的详细数据。") return item_type = item_data.get('type') data = item_data.get('data', {}) if item_type == 'pv': pv_name = data.get('pv_name') if pv_name and pv_name != 'N/A': delete_pv_action = menu.addAction(f"删除物理卷 {pv_name}") delete_pv_action.triggered.connect(lambda: self._handle_delete_pv(pv_name)) elif item_type == 'vg': vg_name = data.get('vg_name') if vg_name and vg_name != 'N/A': delete_vg_action = menu.addAction(f"删除卷组 {vg_name}") delete_vg_action.triggered.connect(lambda: self._handle_delete_vg(vg_name)) elif item_type == 'lv': lv_name = data.get('lv_name') vg_name = data.get('vg_name') lv_attr = data.get('lv_attr', '') lv_path = data.get('lv_path') # e.g., /dev/vgname/lvname # 确保所有关键信息都存在且有效 if lv_name and vg_name and lv_path and lv_path != 'N/A': # Activation/Deactivation if 'a' in lv_attr: # 'a' 表示 active deactivate_lv_action = menu.addAction(f"停用逻辑卷 {lv_name}") deactivate_lv_action.triggered.connect(lambda: self._handle_deactivate_lv(lv_name, vg_name)) else: activate_lv_action = menu.addAction(f"激活逻辑卷 {lv_name}") activate_lv_action.triggered.connect(lambda: self._handle_activate_lv(lv_name, vg_name)) # Mount/Unmount current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path) if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '': unmount_lv_action = menu.addAction(f"卸载 {lv_name} ({current_mount_point})") # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_lv_action.triggered.connect(lambda: self._unmount_and_refresh(lv_path)) else: mount_lv_action = menu.addAction(f"挂载 {lv_name}...") mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path)) menu.addSeparator() # 在挂载/卸载后添加分隔符 # Delete and Format delete_lv_action = menu.addAction(f"删除逻辑卷 {lv_name}") delete_lv_action.triggered.connect(lambda: self._handle_delete_lv(lv_name, vg_name)) format_lv_action = menu.addAction(f"格式化逻辑卷 {lv_name}...") format_lv_action.triggered.connect(lambda: self._handle_format_partition(lv_path)) else: logger.warning(f"逻辑卷 '{lv_name}' (VG: {vg_name}) 的路径无效,无法显示操作。Lv Path: {lv_path}") if menu.actions(): menu.exec(self.ui.treeWidget_lvm.mapToGlobal(pos)) else: logger.info("右键点击了空白区域或没有可用的LVM操作。") def _handle_create_pv(self): available_partitions = self.system_manager.get_unallocated_partitions() if not available_partitions: QMessageBox.warning(self, "警告", "没有可用于创建物理卷的未分配分区。") return dialog = CreatePvDialog(self, available_partitions) if dialog.exec() == QDialog.Accepted: info = dialog.get_pv_info() if info: # Check if info is not None if self.lvm_ops.create_pv(info['device_path']): self.refresh_all_info() def _handle_delete_pv(self, device_path): if self.lvm_ops.delete_pv(device_path): self.refresh_all_info() def _handle_create_vg(self): lvm_info = self.system_manager.get_lvm_info() available_pvs = [] # Filter PVs that are not part of any VG for pv in lvm_info.get('pvs', []): pv_name = pv.get('pv_name') if pv_name and pv_name != 'N/A' and not pv.get('vg_name'): # Check if pv_name is not associated with any VG if pv_name.startswith('/dev/'): available_pvs.append(pv_name) else: # Attempt to construct full path if only name is given available_pvs.append(f"/dev/{pv_name}") if not available_pvs: QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。请确保有未分配给任何卷组的物理卷。") return dialog = CreateVgDialog(self, available_pvs) if dialog.exec() == QDialog.Accepted: info = dialog.get_vg_info() if info: # Check if info is not None if self.lvm_ops.create_vg(info['vg_name'], info['pvs']): self.refresh_all_info() def _handle_delete_vg(self, vg_name): if self.lvm_ops.delete_vg(vg_name): self.refresh_all_info() def _handle_create_lv(self): lvm_info = self.system_manager.get_lvm_info() available_vgs = [] vg_sizes = {} # 存储每个 VG 的可用大小 (GB) for vg in lvm_info.get('vgs', []): vg_name = vg.get('vg_name') if vg_name and vg_name != 'N/A': available_vgs.append(vg_name) free_size_str = vg.get('vg_free', '0B').strip() # 确保去除空白字符 current_vg_size_gb = 0.0 # 为当前VG初始化大小 # LVM输出通常使用 'g', 'm', 't', 'k' 作为单位,而不是 'GB', 'MB' # 匹配数字部分和可选的单位 match = re.match(r'(\d+\.?\d*)\s*([gmkt])?', free_size_str, re.IGNORECASE) if match: value = float(match.group(1)) unit = match.group(2).lower() if match.group(2) else '' # 获取单位,转小写 if unit == 'k': # Kilobytes current_vg_size_gb = value / (1024 * 1024) elif unit == 'm': # Megabytes current_vg_size_gb = value / 1024 elif unit == 'g': # Gigabytes current_vg_size_gb = value elif unit == 't': # Terabytes current_vg_size_gb = value * 1024 elif unit == '': # If no unit, assume GB if it's a large number, or just the value current_vg_size_gb = value # This might be less accurate, but matches common LVM output else: logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'") else: logger.warning(f"无法解析LVM空闲大小字符串: '{free_size_str}'") vg_sizes[vg_name] = current_vg_size_gb if not available_vgs: QMessageBox.warning(self, "警告", "没有可用于创建逻辑卷的卷组。") return dialog = CreateLvDialog(self, available_vgs, vg_sizes) # 传递 vg_sizes if dialog.exec() == QDialog.Accepted: info = dialog.get_lv_info() if info: # 确保 info 不是 None (用户可能在 CreateLvDialog 中取消了操作) # 传递 use_max_space 标志给 lvm_ops.create_lv if self.lvm_ops.create_lv(info['lv_name'], info['vg_name'], info['size_gb'], info['use_max_space']): self.refresh_all_info() def _handle_delete_lv(self, lv_name, vg_name): # 删除 LV 前,也需要确保卸载并从 fstab 移除 # 获取 LV 的完整路径 lv_path = f"/dev/{vg_name}/{lv_name}" # 尝试卸载并从 fstab 移除 (静默,因为删除操作是主要目的) self.disk_ops.unmount_partition(lv_path, show_dialog_on_error=False) self.disk_ops._remove_fstab_entry(lv_path) # 直接调用内部方法,不弹出对话框 if self.lvm_ops.delete_lv(lv_name, vg_name): self.refresh_all_info() def _handle_activate_lv(self, lv_name, vg_name): if self.lvm_ops.activate_lv(lv_name, vg_name): self.refresh_all_info() def _handle_deactivate_lv(self, lv_name, vg_name): if self.lvm_ops.deactivate_lv(lv_name, vg_name): self.refresh_all_info() if __name__ == "__main__": app = QApplication(sys.argv) widget = MainWindow() widget.show() sys.exit(app.exec())