Files
diskmanager/mainwindow.py
2026-02-02 18:38:41 +08:00

755 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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())