This commit is contained in:
zj
2026-02-02 19:48:21 +08:00
parent a90725d178
commit ac809c08df
7 changed files with 448 additions and 540 deletions

View File

@@ -1,8 +1,7 @@
# mainwindow.py
import sys
import logging
import re
import os # 导入 os 模块
import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem,
QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog)
@@ -56,7 +55,6 @@ class MainWindow(QMainWindow):
self.ui.treeWidget_lvm.setContextMenuPolicy(Qt.CustomContextMenu)
self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu)
# 初始化时刷新所有数据
self.refresh_all_info()
logger.info("所有设备信息已初始化加载。")
@@ -126,7 +124,6 @@ class MainWindow(QMainWindow):
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)
@@ -135,7 +132,6 @@ class MainWindow(QMainWindow):
menu.addMenu(create_menu)
menu.addSeparator()
if item:
dev_data = item.data(0, Qt.UserRole)
if not dev_data:
@@ -145,30 +141,25 @@ class MainWindow(QMainWindow):
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提供的完整路径
device_path = dev_data.get('path')
if not device_path: # 如果path字段缺失则尝试从name构造
if not device_path:
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'))) # 传递原始大小字符串
create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data))
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))
@@ -180,9 +171,9 @@ class MainWindow(QMainWindow):
else:
logger.info("右键点击了空白区域或设备没有可用的操作。")
def _handle_create_partition(self, disk_path, total_size_str): # 接收原始大小字符串
# 1. 解析磁盘总大小 (MiB)
def _handle_create_partition(self, disk_path, dev_data):
total_disk_mib = 0.0
total_size_str = dev_data.get('size')
logger.debug(f"尝试为磁盘 {disk_path} 创建分区。原始大小字符串: '{total_size_str}'")
if total_size_str:
@@ -212,70 +203,55 @@ class MainWindow(QMainWindow):
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
start_position_mib = 0.0
max_available_mib = total_disk_mib
# 3. 计算最大可用空间 (MiB)
max_available_mib = total_disk_mib - start_position_mib
if max_available_mib < 0: # 安全检查,理论上不应该发生
max_available_mib = 0.0
if dev_data.get('children'):
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
calculated_start_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path)
if calculated_start_mib is None:
QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
return
start_position_mib = calculated_start_mib
max_available_mib = total_disk_mib - start_position_mib
if max_available_mib < 0:
max_available_mib = 0.0
else:
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。")
max_available_mib = max(0.0, total_disk_mib - 1.0)
start_position_mib = 1.0
logger.debug(f"磁盘 /dev/sdd 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。")
# 确保 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
dialog = CreatePartitionDialog(self, disk_path, 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(
if info:
if 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
info['total_disk_mib'],
info['use_max_space']
):
self.refresh_all_info()
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
if info:
if self.disk_ops.mount_partition(device_path, info['mount_point'], info['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()
@@ -285,7 +261,7 @@ class MainWindow(QMainWindow):
raid_headers = [
"阵列设备", "级别", "状态", "大小", "活动设备", "失败设备", "备用设备",
"总设备数", "UUID", "名称", "Chunk Size", "挂载点" # 添加挂载点列
"总设备数", "UUID", "名称", "Chunk Size", "挂载点"
]
self.ui.treeWidget_raid.setColumnCount(len(raid_headers))
self.ui.treeWidget_raid.setHeaderLabels(raid_headers)
@@ -304,7 +280,6 @@ class MainWindow(QMainWindow):
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
@@ -322,24 +297,19 @@ class MainWindow(QMainWindow):
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.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阵列信息刷新成功。")
@@ -352,30 +322,27 @@ class MainWindow(QMainWindow):
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阵列项
if item and item.parent() is None:
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
array_path = array_data.get('device')
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 # 如果路径无效,则不显示任何操作
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}...")
@@ -389,7 +356,6 @@ class MainWindow(QMainWindow):
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():
@@ -398,8 +364,6 @@ class MainWindow(QMainWindow):
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:
@@ -409,7 +373,7 @@ class MainWindow(QMainWindow):
dialog = CreateRaidDialog(self, available_devices)
if dialog.exec() == QDialog.Accepted:
info = dialog.get_raid_info()
if info: # Check if info is not None
if info:
if self.raid_ops.create_raid_array(info['devices'], info['level'], info['chunk_size']):
self.refresh_all_info()
@@ -422,10 +386,6 @@ class MainWindow(QMainWindow):
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()
@@ -433,7 +393,7 @@ class MainWindow(QMainWindow):
def refresh_lvm_info(self):
self.ui.treeWidget_lvm.clear()
lvm_headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"] # 添加挂载点列
lvm_headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"]
self.ui.treeWidget_lvm.setColumnCount(len(lvm_headers))
self.ui.treeWidget_lvm.setHeaderLabels(lvm_headers)
@@ -453,16 +413,14 @@ class MainWindow(QMainWindow):
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'}) # 标识根节点
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
if pv_name.startswith('/dev/'):
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)
@@ -472,11 +430,10 @@ class MainWindow(QMainWindow):
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_item.setText(7, "")
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}) # 存储原始数据
pv_data_for_context['pv_name'] = pv_path
pv_item.setData(0, Qt.UserRole, {'type': 'pv', 'data': pv_data_for_context})
else:
item = QTreeWidgetItem(pv_root_item)
item.setText(0, "未找到物理卷。")
@@ -497,8 +454,7 @@ class MainWindow(QMainWindow):
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_item.setText(7, "")
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})
@@ -518,14 +474,12 @@ class MainWindow(QMainWindow):
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
lv_path = 'N/A'
current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path)
@@ -535,10 +489,9 @@ class MainWindow(QMainWindow):
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_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
@@ -561,7 +514,6 @@ class MainWindow(QMainWindow):
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)
@@ -595,30 +547,25 @@ class MainWindow(QMainWindow):
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
lv_path = data.get('lv_path')
# 确保所有关键信息都存在且有效
if lv_name and vg_name and lv_path and lv_path != 'N/A':
# Activation/Deactivation
if 'a' in lv_attr: # 'a' 表示 active
if 'a' in lv_attr:
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() # 在挂载/卸载后添加分隔符
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))
@@ -641,7 +588,7 @@ class MainWindow(QMainWindow):
dialog = CreatePvDialog(self, available_partitions)
if dialog.exec() == QDialog.Accepted:
info = dialog.get_pv_info()
if info: # Check if info is not None
if info:
if self.lvm_ops.create_pv(info['device_path']):
self.refresh_all_info()
@@ -652,17 +599,14 @@ class MainWindow(QMainWindow):
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 and pv_name != 'N/A' and not pv.get('vg_name'):
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
@@ -670,43 +614,37 @@ class MainWindow(QMainWindow):
dialog = CreateVgDialog(self, available_pvs)
if dialog.exec() == QDialog.Accepted:
info = dialog.get_vg_info()
if info: # Check if info is not None
if info:
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)
vg_sizes = {}
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初始化大小
free_size_str = vg.get('vg_free', '0B').strip()
current_vg_size_gb = 0.0
# LVM输出通常使用 'g', 'm', 't', 'k' 作为单位,而不是 'GB', 'MB'
# 匹配数字部分和可选的单位
match = re.match(r'(\d+\.?\d*)\s*([gmkt])?', free_size_str, re.IGNORECASE)
match = re.match(r'(\d+\.?\d*)\s*([gmktb])?', free_size_str, re.IGNORECASE)
if match:
value = float(match.group(1))
unit = match.group(2).lower() if match.group(2) else '' # 获取单位,转小写
unit = match.group(2).lower() if match.group(2) else ''
if unit == 'k': # Kilobytes
if unit == 'k':
current_vg_size_gb = value / (1024 * 1024)
elif unit == 'm': # Megabytes
elif unit == 'm':
current_vg_size_gb = value / 1024
elif unit == 'g': # Gigabytes
elif unit == 'g':
current_vg_size_gb = value
elif unit == 't': # Terabytes
elif unit == 't':
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
elif unit == 'b' or unit == '':
current_vg_size_gb = value / (1024 * 1024 * 1024)
else:
logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'")
else:
@@ -714,26 +652,21 @@ class MainWindow(QMainWindow):
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
dialog = CreateLvDialog(self, available_vgs, 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 info:
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) # 直接调用内部方法,不弹出对话框
self.disk_ops._remove_fstab_entry(lv_path)
if self.lvm_ops.delete_lv(lv_name, vg_name):
self.refresh_all_info()
@@ -746,6 +679,42 @@ class MainWindow(QMainWindow):
if self.lvm_ops.deactivate_lv(lv_name, vg_name):
self.refresh_all_info()
def _handle_delete_vg(self, vg_name):
"""
处理删除卷组 (VG) 的操作。
"""
logger.info(f"尝试删除卷组 (VG) {vg_name}")
reply = QMessageBox.question(
self,
"确认删除卷组",
f"您确定要删除卷组 {vg_name} 吗?此操作将永久删除该卷组及其所有逻辑卷和数据!",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
logger.info(f"用户取消了删除卷组 {vg_name} 的操作。")
return
lvm_info = self.system_manager.get_lvm_info()
vg_info = next((vg for vg in lvm_info.get('vgs', []) if vg.get('vg_name') == vg_name), None)
lv_count_raw = vg_info.get('lv_count', 0) if vg_info else 0
try:
lv_count = int(float(lv_count_raw))
except (ValueError, TypeError):
lv_count = 0
if lv_count > 0:
QMessageBox.critical(self, "删除失败", f"卷组 {vg_name} 中仍包含逻辑卷。请先删除所有逻辑卷。")
logger.error(f"尝试删除包含逻辑卷的卷组 {vg_name}。操作被阻止。")
return
success = self.lvm_ops.delete_vg(vg_name)
if success:
self.refresh_all_info()
else:
QMessageBox.critical(self, "删除卷组失败", f"删除卷组 {vg_name} 失败,请检查日志。")
if __name__ == "__main__":
app = QApplication(sys.argv)