fix33
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
25
dialogs.py
25
dialogs.py
@@ -20,8 +20,9 @@ class CreatePartitionDialog(QDialog):
|
||||
self.partition_table_type_combo.addItems(["gpt", "msdos"])
|
||||
|
||||
self.size_spinbox = QDoubleSpinBox()
|
||||
self.size_spinbox.setMinimum(0.1) # 最小分区大小 0.1 GB
|
||||
self.size_spinbox.setMaximum(self.max_available_mib / 1024.0) # 将 MiB 转换为 GB 显示
|
||||
# 调整最小值为 0.01 GB (约 10MB),并确保最大值不小于最小值
|
||||
self.size_spinbox.setMinimum(0.01)
|
||||
self.size_spinbox.setMaximum(max(0.01, self.max_available_mib / 1024.0)) # 将 MiB 转换为 GB 显示
|
||||
self.size_spinbox.setSuffix(" GB")
|
||||
self.size_spinbox.setDecimals(2)
|
||||
|
||||
@@ -76,7 +77,8 @@ class CreatePartitionDialog(QDialog):
|
||||
if not use_max_space and size_gb <= 0:
|
||||
QMessageBox.warning(self, "输入错误", "分区大小必须大于0。")
|
||||
return None
|
||||
if not use_max_space and size_gb > self.max_available_mib / 1024.0:
|
||||
# 这里的检查应该使用 self.size_spinbox.maximum() 来判断,因为它是实际的最大值
|
||||
if not use_max_space and size_gb > self.size_spinbox.maximum():
|
||||
QMessageBox.warning(self, "输入错误", "分区大小不能超过最大可用空间。")
|
||||
return None
|
||||
|
||||
@@ -387,20 +389,21 @@ class CreateLvDialog(QDialog):
|
||||
|
||||
# 确保 max_size_gb 至少是 spinbox 的最小值,以防卷组可用空间过小导致 UI 问题
|
||||
if max_size_gb < self.lv_size_spinbox.minimum():
|
||||
self.lv_size_spinbox.setMinimum(max_size_gb) # 临时将最小值设为实际最大值
|
||||
# 如果实际最大可用空间小于最小允许值,则将最小值临时调整为实际最大值
|
||||
self.lv_size_spinbox.setMinimum(max_size_gb if max_size_gb > 0 else 0.01) # 至少0.01GB
|
||||
else:
|
||||
self.lv_size_spinbox.setMinimum(0.1) # 恢复正常最小值
|
||||
|
||||
self.lv_size_spinbox.setMaximum(max_size_gb) # 设置最大值
|
||||
self.lv_size_spinbox.setMaximum(max(self.lv_size_spinbox.minimum(), max_size_gb)) # 设置最大值,确保不小于最小值
|
||||
|
||||
# 如果选中了“使用最大可用空间”,则将 spinbox 值设置为最大值
|
||||
if self.use_max_space_checkbox.isChecked():
|
||||
self.lv_size_spinbox.setValue(max_size_gb)
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum())
|
||||
else:
|
||||
# 如果当前值超过了新的最大值,则调整为新的最大值
|
||||
if self.lv_size_spinbox.value() > max_size_gb:
|
||||
self.lv_size_spinbox.setValue(max_size_gb)
|
||||
# 如果当前值小于新的最小值 (例如,VG可用空间变为0,最小值被调整为0),则调整
|
||||
if self.lv_size_spinbox.value() > self.lv_size_spinbox.maximum():
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum())
|
||||
# 如果当前值小于新的最小值,则调整
|
||||
elif self.lv_size_spinbox.value() < self.lv_size_spinbox.minimum():
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum())
|
||||
|
||||
@@ -432,7 +435,8 @@ class CreateLvDialog(QDialog):
|
||||
if not use_max_space and size_gb <= 0:
|
||||
QMessageBox.warning(self, "输入错误", "逻辑卷大小必须大于0。")
|
||||
return None
|
||||
if not use_max_space and size_gb > self.vg_sizes.get(vg_name, 0.0):
|
||||
# 这里的检查应该使用 self.lv_size_spinbox.maximum() 来判断,因为它是实际的最大值
|
||||
if not use_max_space and size_gb > self.lv_size_spinbox.maximum():
|
||||
QMessageBox.warning(self, "输入错误", "逻辑卷大小不能超过卷组的可用空间。")
|
||||
return None
|
||||
|
||||
@@ -442,4 +446,3 @@ class CreateLvDialog(QDialog):
|
||||
'size_gb': size_gb,
|
||||
'use_max_space': use_max_space # 返回此标志
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
# disk_operations.py
|
||||
import subprocess
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import os
|
||||
from PySide6.QtWidgets import QMessageBox, QInputDialog
|
||||
|
||||
# 导入我们自己编写的系统信息管理模块
|
||||
from system_info import SystemInfoManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DiskOperations:
|
||||
def __init__(self):
|
||||
self.system_manager = SystemInfoManager() # 实例化 SystemInfoManager
|
||||
pass
|
||||
|
||||
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=None, input_data=None):
|
||||
@@ -25,7 +21,6 @@ class DiskOperations:
|
||||
:param input_data: 传递给命令stdin的数据 (str)。
|
||||
:return: (True/False, stdout_str, stderr_str)
|
||||
"""
|
||||
# 确保 command_list 中的所有项都是字符串
|
||||
if not all(isinstance(arg, str) for arg in command_list):
|
||||
logger.error(f"命令列表包含非字符串元素: {command_list}")
|
||||
QMessageBox.critical(None, "错误", f"内部错误:尝试执行的命令包含无效参数。\n命令详情: {command_list}")
|
||||
@@ -56,7 +51,9 @@ class DiskOperations:
|
||||
logger.error(f"标准错误: {stderr_output}")
|
||||
|
||||
if suppress_critical_dialog_on_stderr_match and \
|
||||
suppress_critical_dialog_on_stderr_match in stderr_output:
|
||||
(suppress_critical_dialog_on_stderr_match in stderr_output or \
|
||||
(isinstance(suppress_critical_dialog_on_stderr_match, tuple) and \
|
||||
any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match))):
|
||||
logger.info(f"错误信息 '{stderr_output}' 匹配抑制条件,不显示关键错误对话框。")
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}")
|
||||
@@ -70,213 +67,356 @@ class DiskOperations:
|
||||
logger.error(f"执行命令 {full_cmd_str} 时发生未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def _get_fstab_path(self):
|
||||
return "/etc/fstab"
|
||||
def _add_to_fstab(self, device_path, mount_point, fstype, uuid):
|
||||
"""
|
||||
将设备的挂载信息添加到 /etc/fstab。
|
||||
使用 UUID 识别设备以提高稳定性。
|
||||
"""
|
||||
if not uuid or not mount_point or not fstype:
|
||||
logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。")
|
||||
QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。")
|
||||
return False
|
||||
|
||||
fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2"
|
||||
fstab_path = "/etc/fstab"
|
||||
|
||||
try:
|
||||
# 检查 fstab 中是否已存在相同 UUID 或挂载点的条目
|
||||
with open(fstab_path, 'r') as f:
|
||||
fstab_content = f.readlines()
|
||||
|
||||
for line in fstab_content:
|
||||
if f"UUID={uuid}" in line:
|
||||
logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。")
|
||||
QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。")
|
||||
return True # 认为成功,因为目标已达成
|
||||
# 检查挂载点是否已存在 (可能被其他设备使用)
|
||||
if mount_point in line.split():
|
||||
logger.warning(f"挂载点 {mount_point} 已存在于 fstab 中。跳过添加。")
|
||||
QMessageBox.information(None, "信息", f"挂载点 {mount_point} 已存在于 fstab 中。")
|
||||
return True # 认为成功,因为目标已达成
|
||||
|
||||
# 如果不存在,则追加到 fstab
|
||||
with open(fstab_path, 'a') as f:
|
||||
f.write(fstab_entry + '\n')
|
||||
logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。")
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"写入 {fstab_path} 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"写入 {fstab_path} 失败: {e}")
|
||||
return False
|
||||
|
||||
def _remove_fstab_entry(self, device_path):
|
||||
"""
|
||||
从 /etc/fstab 中移除指定设备的条目。
|
||||
此方法需要 SystemInfoManager 来获取设备的 UUID。
|
||||
"""
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["lsblk", "-no", "UUID", device_path],
|
||||
f"获取设备 {device_path} 的 UUID 失败",
|
||||
root_privilege=False, # lsblk 通常不需要 sudo
|
||||
suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") # 匹配中英文错误
|
||||
)
|
||||
if not success:
|
||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。错误: {stderr}")
|
||||
return False
|
||||
|
||||
uuid = stdout.strip()
|
||||
if not uuid:
|
||||
logger.warning(f"设备 {device_path} 没有 UUID,无法从 fstab 中移除。")
|
||||
return False
|
||||
|
||||
fstab_path = "/etc/fstab"
|
||||
try:
|
||||
with open(fstab_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
removed = False
|
||||
for line in lines:
|
||||
if f"UUID={uuid}" in line:
|
||||
logger.info(f"从 {fstab_path} 中移除了条目: {line.strip()}")
|
||||
removed = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if removed:
|
||||
# 写入临时文件,然后替换原文件
|
||||
with open(fstab_path + ".tmp", 'w') as f_tmp:
|
||||
f_tmp.writelines(new_lines)
|
||||
os.rename(fstab_path + ".tmp", fstab_path)
|
||||
logger.info(f"已从 {fstab_path} 中移除 UUID={uuid} 的条目。")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"fstab 中未找到 UUID={uuid} 的条目。")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"修改 {fstab_path} 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"修改 {fstab_path} 失败: {e}")
|
||||
return False
|
||||
|
||||
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
||||
"""
|
||||
挂载指定设备到指定挂载点。
|
||||
:param device_path: 要挂载的设备路径。
|
||||
:param mount_point: 挂载点。
|
||||
:param add_to_fstab: 是否添加到 /etc/fstab。
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
if not os.path.exists(mount_point):
|
||||
try:
|
||||
os.makedirs(mount_point)
|
||||
logger.info(f"创建挂载点目录: {mount_point}")
|
||||
except OSError as e:
|
||||
QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}")
|
||||
logger.error(f"创建挂载点目录 {mount_point} 失败: {e}")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["mount", device_path, mount_point],
|
||||
f"挂载设备 {device_path} 失败"
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
||||
if add_to_fstab:
|
||||
# 为了获取 fstype 和 UUID,需要再次调用 lsblk
|
||||
success_details, stdout_details, stderr_details = self._execute_shell_command(
|
||||
["lsblk", "-no", "FSTYPE,UUID", device_path],
|
||||
f"获取设备 {device_path} 的文件系统类型和 UUID 失败",
|
||||
root_privilege=False,
|
||||
suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address")
|
||||
)
|
||||
if success_details:
|
||||
fstype, uuid = stdout_details.strip().split()
|
||||
self._add_to_fstab(device_path, mount_point, fstype, uuid)
|
||||
else:
|
||||
logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab: {stderr_details}")
|
||||
QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def unmount_partition(self, device_path, show_dialog_on_error=True):
|
||||
"""
|
||||
卸载指定设备。
|
||||
:param device_path: 要卸载的设备路径。
|
||||
:param show_dialog_on_error: 是否在发生错误时显示对话框。
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
logger.info(f"尝试卸载设备 {device_path}。")
|
||||
try:
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
else:
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"卸载设备 {device_path} 时发生异常: {e}")
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 时发生异常: {e}")
|
||||
return False
|
||||
|
||||
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
|
||||
"""
|
||||
获取磁盘上最大的空闲空间块的起始位置 (MiB) 和大小 (MiB)。
|
||||
:param disk_path: 磁盘路径。
|
||||
:param total_disk_mib: 磁盘的总大小 (MiB),用于全新磁盘的计算。
|
||||
:return: (start_mib, size_mib) 元组。如果磁盘是全新的,返回 (0.0, total_disk_mib)。
|
||||
如果磁盘有分区表但没有空闲空间,返回 (None, None)。
|
||||
"""
|
||||
logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
|
||||
f"获取磁盘 {disk_path} 分区信息失败",
|
||||
root_privilege=True
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}")
|
||||
return None, None
|
||||
|
||||
logger.debug(f"parted print free 命令原始输出:\n{stdout}")
|
||||
|
||||
free_spaces = []
|
||||
lines = stdout.splitlines()
|
||||
# Regex to capture StartMiB and SizeMiB from a "Free Space" line
|
||||
# It's made more flexible to match "Free Space", "空闲空间", and "可用空间"
|
||||
# Example: " 0.02MiB 8192MiB 8192MiB 可用空间"
|
||||
# We need to capture the first numeric value (Start) and the third numeric value (Size).
|
||||
free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)')
|
||||
|
||||
for line in lines:
|
||||
match = free_space_line_pattern.match(line)
|
||||
if match:
|
||||
try:
|
||||
start_mib = float(match.group(1)) # Capture StartMiB
|
||||
size_mib = float(match.group(3)) # Capture SizeMiB (the third numeric value)
|
||||
free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib})
|
||||
except ValueError as ve:
|
||||
logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}")
|
||||
continue
|
||||
|
||||
if not free_spaces:
|
||||
logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。")
|
||||
return None, None
|
||||
|
||||
# 找到最大的空闲空间块
|
||||
largest_free_space = max(free_spaces, key=lambda x: x['size_mib'])
|
||||
start_mib = largest_free_space['start_mib']
|
||||
size_mib = largest_free_space['size_mib']
|
||||
|
||||
logger.debug(f"磁盘 {disk_path} 的最大空闲空间块起始于 {start_mib:.2f} MiB,大小为 {size_mib:.2f} MiB。")
|
||||
return start_mib, size_mib
|
||||
|
||||
def create_partition(self, disk_path, partition_table_type, size_gb, total_disk_mib, use_max_space):
|
||||
"""
|
||||
在指定磁盘上创建分区。
|
||||
:param disk_path: 磁盘设备路径,例如 /dev/sdb。
|
||||
:param partition_table_type: 分区表类型,'gpt' 或 'msdos'。
|
||||
:param size_gb: 分区大小(GB)。
|
||||
:param disk_path: 磁盘路径 (例如 /dev/sdb)。
|
||||
:param partition_table_type: 分区表类型 ('gpt' 或 'msdos')。
|
||||
:param size_gb: 分区大小 (GB)。
|
||||
:param total_disk_mib: 磁盘总大小 (MiB)。
|
||||
:param use_max_space: 是否使用最大可用空间创建分区。
|
||||
:return: 新创建的分区路径,如果失败则为 None。
|
||||
:param use_max_space: 是否使用最大可用空间。
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
if not isinstance(disk_path, str) or not isinstance(partition_table_type, str):
|
||||
logger.error(f"尝试创建分区时传入无效的磁盘路径或分区表类型。磁盘: {disk_path} (类型: {type(disk_path)}), 类型: {partition_table_type} (类型: {type(partition_table_type)})")
|
||||
logger.error(f"尝试创建分区时传入无效参数。磁盘路径: {disk_path}, 分区表类型: {partition_table_type}")
|
||||
QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。")
|
||||
return None
|
||||
return False
|
||||
|
||||
logger.info(f"尝试在 {disk_path} 上创建 {size_gb}GB 的分区,分区表类型为 {partition_table_type}。")
|
||||
|
||||
# 1. 检查磁盘是否已经有分区表,如果没有则创建
|
||||
# parted -s /dev/sdb print
|
||||
# 1. 检查磁盘是否有分区表
|
||||
has_partition_table = False
|
||||
try:
|
||||
stdout, _ = self.system_manager._run_command(["parted", "-s", disk_path, "print"], root_privilege=True)
|
||||
if "unrecognised disk label" in stdout:
|
||||
logger.info(f"磁盘 {disk_path} 没有分区表,将创建 {partition_table_type} 分区表。")
|
||||
success, _, _ = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "mklabel", partition_table_type],
|
||||
f"创建分区表 {partition_table_type} 失败"
|
||||
)
|
||||
if not success:
|
||||
return None
|
||||
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "print"],
|
||||
f"检查磁盘 {disk_path} 分区表失败",
|
||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
||||
root_privilege=True
|
||||
)
|
||||
if success_check:
|
||||
if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check:
|
||||
has_partition_table = True
|
||||
else:
|
||||
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。")
|
||||
else:
|
||||
logger.info(f"磁盘 {disk_path} 已有分区表。")
|
||||
logger.info(f"parted print 命令失败,但可能不是因为没有分区表,错误信息: {stderr_check}")
|
||||
except Exception as e:
|
||||
logger.error(f"检查或创建分区表失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"检查或创建分区表失败: {e}")
|
||||
return None
|
||||
logger.error(f"检查磁盘 {disk_path} 分区表时发生异常: {e}")
|
||||
pass
|
||||
|
||||
# 2. 获取下一个分区的起始扇区
|
||||
start_sector = self.get_disk_next_partition_start_sector(disk_path)
|
||||
if start_sector is None:
|
||||
QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
||||
return None
|
||||
actual_start_mib_for_parted = 0.0
|
||||
|
||||
# 3. 构建 parted 命令
|
||||
parted_cmd = ["parted", "-s", disk_path, "mkpart", "primary"]
|
||||
# 2. 如果没有分区表,则创建分区表
|
||||
if not has_partition_table:
|
||||
reply = QMessageBox.question(None, "确认创建分区表",
|
||||
f"磁盘 {disk_path} 没有分区表。您确定要创建 {partition_table_type} 分区表吗?"
|
||||
f"此操作将擦除磁盘上的所有数据。",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了在 {disk_path} 上创建分区表的操作。")
|
||||
return False
|
||||
|
||||
# 对于 GPT,默认文件系统是 ext2。对于 msdos,默认是 ext2。
|
||||
# parted mkpart primary [fstype] start end
|
||||
# 我们可以省略 fstype,让用户稍后格式化。
|
||||
|
||||
# 计算结束位置
|
||||
start_mib = start_sector * 512 / (1024 * 1024) # 转换为 MiB
|
||||
|
||||
if use_max_space:
|
||||
end_mib = total_disk_mib # 使用磁盘总大小作为结束位置
|
||||
parted_cmd.extend([f"{start_mib}MiB", f"{end_mib}MiB"])
|
||||
logger.info(f"尝试在 {disk_path} 上创建 {partition_table_type} 分区表。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "mklabel", partition_table_type],
|
||||
f"创建 {partition_table_type} 分区表失败"
|
||||
)
|
||||
if not success:
|
||||
return False
|
||||
# 对于新创建分区表的磁盘,第一个分区通常从 1MiB 开始以确保兼容性和对齐
|
||||
actual_start_mib_for_parted = 1.0
|
||||
else:
|
||||
end_mib = start_mib + size_gb * 1024 # 将 GB 转换为 MiB
|
||||
parted_cmd.extend([f"{start_mib}MiB", f"{end_mib}MiB"])
|
||||
# 如果有分区表,获取下一个可用分区的起始位置
|
||||
start_mib_from_parted, _ = self.get_disk_free_space_info_mib(disk_path, total_disk_mib)
|
||||
if start_mib_from_parted is None:
|
||||
QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。")
|
||||
return False
|
||||
|
||||
# 执行创建分区命令
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
parted_cmd,
|
||||
# 如果 parted 报告的起始位置非常接近 0 MiB (例如 0.0 MiB 或 0.02 MiB),
|
||||
# 为了安全和兼容性,也将其调整为 1.0 MiB。
|
||||
# 否则,使用 parted 报告的精确起始位置。
|
||||
if start_mib_from_parted < 1.0: # covers 0.0MiB and values like 0.017MiB (17.4kB)
|
||||
actual_start_mib_for_parted = 1.0
|
||||
else:
|
||||
actual_start_mib_for_parted = start_mib_from_parted
|
||||
|
||||
# 3. 确定分区结束位置
|
||||
if use_max_space:
|
||||
end_pos = "100%"
|
||||
size_for_log = "最大可用空间"
|
||||
else:
|
||||
# 计算结束 MiB
|
||||
# 注意:这里计算的 end_mib 是基于用户请求的大小,
|
||||
# parted 会根据实际可用空间和对齐进行微调。
|
||||
end_mib = actual_start_mib_for_parted + size_gb * 1024
|
||||
|
||||
# 简单检查,如果请求的大小导致结束位置超出磁盘总大小,则使用最大可用
|
||||
if end_mib > total_disk_mib:
|
||||
QMessageBox.warning(None, "警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。")
|
||||
end_pos = "100%"
|
||||
size_for_log = "最大可用空间"
|
||||
else:
|
||||
end_pos = f"{end_mib}MiB"
|
||||
size_for_log = f"{size_gb}GB"
|
||||
|
||||
# 4. 创建分区
|
||||
create_cmd = ["parted", "-s", disk_path, "mkpart", "primary", f"{actual_start_mib_for_parted}MiB", end_pos]
|
||||
logger.info(f"尝试在 {disk_path} 上创建 {size_for_log} 的主分区。命令: {' '.join(create_cmd)}")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
create_cmd,
|
||||
f"在 {disk_path} 上创建分区失败"
|
||||
)
|
||||
if not success:
|
||||
return None
|
||||
return False
|
||||
|
||||
# 4. 刷新内核分区表
|
||||
self._execute_shell_command(["partprobe", disk_path], f"刷新 {disk_path} 分区表失败", root_privilege=True)
|
||||
|
||||
# 5. 获取新创建的分区路径
|
||||
# 通常新分区是磁盘的最后一个分区,例如 /dev/sdb1, /dev/sdb2 等
|
||||
# 我们可以通过 lsblk 再次获取信息,找到最新的分区
|
||||
try:
|
||||
# 重新获取设备信息,找到最新的分区
|
||||
devices = self.system_manager.get_block_devices()
|
||||
new_partition_path = None
|
||||
for dev in devices:
|
||||
if dev.get('path') == disk_path and dev.get('children'):
|
||||
# 找到最新的子分区
|
||||
latest_partition = None
|
||||
for child in dev['children']:
|
||||
if child.get('type') == 'part':
|
||||
if latest_partition is None or int(child.get('maj:min').split(':')[1]) > int(latest_partition.get('maj:min').split(':')[1]):
|
||||
latest_partition = child
|
||||
if latest_partition:
|
||||
new_partition_path = latest_partition.get('path')
|
||||
break
|
||||
|
||||
if new_partition_path:
|
||||
logger.info(f"成功在 {disk_path} 上创建分区: {new_partition_path}")
|
||||
return new_partition_path
|
||||
else:
|
||||
logger.error(f"在 {disk_path} 上创建分区成功,但未能确定新分区的设备路径。")
|
||||
QMessageBox.warning(None, "警告", f"分区创建成功,但未能确定新分区的设备路径。请手动检查。")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取新创建分区路径失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"获取新创建分区路径失败: {e}")
|
||||
return None
|
||||
|
||||
def get_disk_next_partition_start_sector(self, disk_path):
|
||||
"""
|
||||
获取磁盘上下一个可用的分区起始扇区。
|
||||
:param disk_path: 磁盘设备路径,例如 /dev/sdb。
|
||||
:return: 下一个分区起始扇区(整数),如果失败则为 None。
|
||||
"""
|
||||
if not isinstance(disk_path, str):
|
||||
logger.error(f"传入 get_disk_next_partition_start_sector 的 disk_path 不是字符串: {disk_path} (类型: {type(disk_path)})")
|
||||
return None
|
||||
|
||||
try:
|
||||
stdout, _ = self.system_manager._run_command(["parted", "-s", disk_path, "unit", "s", "print", "free"], root_privilege=True)
|
||||
# 查找最大的空闲空间
|
||||
free_space_pattern = re.compile(r'^\s*\d+\s+([\d.]+s)\s+([\d.]+s)\s+([\d.]+s)\s+Free Space', re.MULTILINE)
|
||||
matches = free_space_pattern.findall(stdout)
|
||||
|
||||
if matches:
|
||||
# 找到所有空闲空间的起始扇区,并取最小的作为下一个分区的起始
|
||||
# 或者,更准确地说,找到最后一个分区的结束扇区 + 1
|
||||
# 简化处理:如果没有分区,从 2048s 开始 (常见对齐)
|
||||
# 如果有分区,从最后一个分区的结束扇区 + 1 开始
|
||||
|
||||
# 先尝试获取所有分区信息
|
||||
stdout_partitions, _ = self.system_manager._run_command(["parted", "-s", disk_path, "unit", "s", "print"], root_privilege=True)
|
||||
partition_end_pattern = re.compile(r'^\s*\d+\s+[\d.]+s\s+([\d.]+s)', re.MULTILINE)
|
||||
partition_end_sectors = [int(float(s.replace('s', ''))) for s in partition_end_pattern.findall(stdout_partitions)]
|
||||
|
||||
if partition_end_sectors:
|
||||
# 最后一个分区的结束扇区 + 1
|
||||
last_end_sector = max(partition_end_sectors)
|
||||
# 确保对齐,通常是 1MiB (2048扇区) 或 4MiB (8192扇区)
|
||||
# 这里我们简单地取下一个 1MiB 对齐的扇区
|
||||
start_sector = (last_end_sector // 2048 + 1) * 2048
|
||||
logger.debug(f"磁盘 {disk_path} 最后一个分区结束于 {last_end_sector}s,下一个分区起始扇区建议为 {start_sector}s。")
|
||||
return start_sector
|
||||
else:
|
||||
# 没有分区,从 2048s 开始
|
||||
logger.debug(f"磁盘 {disk_path} 没有现有分区,建议起始扇区为 2048s。")
|
||||
return 2048 # 2048扇区是 1MiB,常见的起始对齐位置
|
||||
else:
|
||||
logger.warning(f"无法在磁盘 {disk_path} 上找到空闲空间信息。")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取磁盘 {disk_path} 下一个分区起始扇区失败: {e}")
|
||||
return None
|
||||
|
||||
def get_disk_next_partition_start_mib(self, disk_path):
|
||||
"""
|
||||
获取磁盘上下一个可用的分区起始位置 (MiB)。
|
||||
:param disk_path: 磁盘设备路径,例如 /dev/sdb。
|
||||
:return: 下一个分区起始位置 (MiB),如果失败则为 None。
|
||||
"""
|
||||
start_sector = self.get_disk_next_partition_start_sector(disk_path)
|
||||
if start_sector is not None:
|
||||
return start_sector * 512 / (1024 * 1024) # 转换为 MiB
|
||||
return None
|
||||
QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。")
|
||||
return True
|
||||
|
||||
def delete_partition(self, device_path):
|
||||
"""
|
||||
删除指定的分区。
|
||||
:param device_path: 要删除的分区路径,例如 /dev/sdb1。
|
||||
:return: True 如果删除成功,否则 False。
|
||||
删除指定分区。
|
||||
:param device_path: 要删除的分区路径。
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
if not isinstance(device_path, str):
|
||||
logger.error(f"尝试删除非字符串设备路径: {device_path} (类型: {type(device_path)})")
|
||||
QMessageBox.critical(None, "错误", "无效的设备路径。")
|
||||
return False
|
||||
|
||||
reply = QMessageBox.question(None, "确认删除分区",
|
||||
f"您确定要删除分区 {device_path} 吗?此操作不可逆!",
|
||||
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除分区: {device_path}")
|
||||
# 尝试卸载分区
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载
|
||||
|
||||
# 1. 尝试卸载分区 (静默处理,如果未挂载则不报错)
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False)
|
||||
|
||||
# 2. 从 fstab 中移除条目
|
||||
# 从 fstab 中移除条目
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
# 3. 获取父磁盘和分区号
|
||||
match = re.match(r'(/dev/\w+)(\d+)', device_path)
|
||||
# 获取父磁盘和分区号
|
||||
# 例如 /dev/sdb1 -> /dev/sdb, 1
|
||||
match = re.match(r'(/dev/[a-z]+)(\d+)', device_path)
|
||||
if not match:
|
||||
QMessageBox.critical(None, "错误", f"无法解析分区路径 {device_path}。")
|
||||
logger.error(f"无法解析分区路径 {device_path}。")
|
||||
QMessageBox.critical(None, "错误", f"无法解析设备路径 {device_path}。")
|
||||
logger.error(f"无法解析设备路径 {device_path}。")
|
||||
return False
|
||||
|
||||
disk_path = match.group(1)
|
||||
partition_number = match.group(2)
|
||||
|
||||
# 4. 执行 parted 命令删除分区
|
||||
logger.info(f"尝试删除分区 {device_path} (磁盘: {disk_path}, 分区号: {partition_number})。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "rm", partition_number],
|
||||
f"删除分区 {device_path} 失败"
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"分区 {device_path} 已成功删除。")
|
||||
# 刷新内核分区表
|
||||
self._execute_shell_command(["partprobe", disk_path], f"刷新 {disk_path} 分区表失败", root_privilege=True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -284,48 +424,38 @@ class DiskOperations:
|
||||
def format_partition(self, device_path, fstype=None):
|
||||
"""
|
||||
格式化指定分区。
|
||||
:param device_path: 要格式化的设备路径,例如 /dev/sdb1。
|
||||
:param fstype: 文件系统类型,例如 'ext4', 'xfs'。如果为 None,则弹出对话框让用户选择。
|
||||
:return: True 如果格式化成功,否则 False。
|
||||
:param device_path: 要格式化的分区路径。
|
||||
:param fstype: 文件系统类型 (例如 'ext4', 'xfs', 'fat32', 'ntfs')。如果为 None,则弹出对话框让用户选择。
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
if not isinstance(device_path, str):
|
||||
logger.error(f"尝试格式化非字符串设备路径: {device_path} (类型: {type(device_path)})")
|
||||
QMessageBox.critical(None, "错误", "无效的设备路径。")
|
||||
return False
|
||||
|
||||
# 1. 尝试卸载分区 (静默处理,如果未挂载则不报错)
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False)
|
||||
|
||||
# 2. 从 fstab 中移除旧条目 (因为格式化后 UUID 会变)
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
if fstype is None:
|
||||
# 弹出对话框让用户选择文件系统类型
|
||||
items = ["ext4", "xfs", "fat32", "ntfs"] # 常用文件系统
|
||||
selected_fstype, ok = QInputDialog.getItem(None, "选择文件系统",
|
||||
f"请为 {device_path} 选择文件系统类型:",
|
||||
items, 0, False)
|
||||
if not ok or not selected_fstype:
|
||||
logger.info(f"用户取消了格式化 {device_path} 的操作。")
|
||||
items = ("ext4", "xfs", "fat32", "ntfs")
|
||||
fstype, ok = QInputDialog.getItem(None, "选择文件系统", "请选择要使用的文件系统类型:", items, 0, False)
|
||||
if not ok or not fstype:
|
||||
logger.info("用户取消了文件系统选择。")
|
||||
return False
|
||||
fstype = selected_fstype
|
||||
|
||||
reply = QMessageBox.question(None, "确认格式化分区",
|
||||
f"您确定要格式化分区 {device_path} 为 {fstype} 吗?此操作将擦除所有数据!",
|
||||
f"您确定要将分区 {device_path} 格式化为 {fstype} 吗?此操作将擦除分区上的所有数据!",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了格式化分区 {device_path} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试格式化设备 {device_path} 为 {fstype}。")
|
||||
# 尝试卸载分区
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载
|
||||
|
||||
# 从 fstab 中移除条目 (因为格式化会改变 UUID)
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
logger.info(f"尝试将分区 {device_path} 格式化为 {fstype}。")
|
||||
format_cmd = []
|
||||
if fstype == "ext4":
|
||||
format_cmd = ["mkfs.ext4", "-F", device_path]
|
||||
format_cmd = ["mkfs.ext4", "-F", device_path] # -F 强制执行
|
||||
elif fstype == "xfs":
|
||||
format_cmd = ["mkfs.xfs", "-f", device_path]
|
||||
format_cmd = ["mkfs.xfs", "-f", device_path] # -f 强制执行
|
||||
elif fstype == "fat32":
|
||||
format_cmd = ["mkfs.vfat", "-F", "32", device_path]
|
||||
format_cmd = ["mkfs.fat", "-F", "32", device_path]
|
||||
elif fstype == "ntfs":
|
||||
format_cmd = ["mkfs.ntfs", "-f", device_path]
|
||||
else:
|
||||
@@ -335,202 +465,11 @@ class DiskOperations:
|
||||
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
format_cmd,
|
||||
f"格式化设备 {device_path} 为 {fstype} 失败"
|
||||
f"格式化分区 {device_path} 失败"
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功格式化为 {fstype}。")
|
||||
QMessageBox.information(None, "成功", f"分区 {device_path} 已成功格式化为 {fstype}。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
||||
"""
|
||||
挂载一个分区。
|
||||
:param device_path: 要挂载的设备路径,例如 /dev/sdb1。
|
||||
:param mount_point: 挂载点,例如 /mnt/data。
|
||||
:param add_to_fstab: 是否将挂载信息添加到 /etc/fstab。
|
||||
:return: True 如果挂载成功,否则 False。
|
||||
"""
|
||||
if not isinstance(device_path, str) or not isinstance(mount_point, str):
|
||||
logger.error(f"尝试挂载时传入无效的设备路径或挂载点。设备: {device_path} (类型: {type(device_path)}), 挂载点: {mount_point} (类型: {type(mount_point)})")
|
||||
QMessageBox.critical(None, "错误", "无效的设备路径或挂载点。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。")
|
||||
try:
|
||||
# 确保挂载点存在
|
||||
if not os.path.exists(mount_point):
|
||||
logger.debug(f"创建挂载点目录: {mount_point}")
|
||||
os.makedirs(mount_point, exist_ok=True)
|
||||
|
||||
# 执行挂载命令
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["mount", device_path, mount_point],
|
||||
f"挂载设备 {device_path} 到 {mount_point} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
||||
if add_to_fstab:
|
||||
self._add_to_fstab(device_path, mount_point)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"挂载设备 {device_path} 时发生错误: {e}")
|
||||
QMessageBox.critical(None, "错误", f"挂载设备 {device_path} 失败。\n错误详情: {e}")
|
||||
return False
|
||||
|
||||
def unmount_partition(self, device_path, show_dialog_on_error=True):
|
||||
"""
|
||||
卸载一个分区。
|
||||
:param device_path: 要卸载的设备路径,例如 /dev/sdb1 或 /dev/md0。
|
||||
:param show_dialog_on_error: 是否在卸载失败时显示错误对话框。
|
||||
:return: True 如果卸载成功,否则 False。
|
||||
"""
|
||||
if not isinstance(device_path, str):
|
||||
logger.error(f"尝试卸载非字符串设备路径: {device_path} (类型: {type(device_path)})")
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"无效的设备路径: {device_path}")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试卸载设备: {device_path}")
|
||||
try:
|
||||
# 尝试从 fstab 中移除条目,如果存在的话
|
||||
# 注意:这里先移除 fstab 条目,是为了防止卸载失败后 fstab 中仍然有无效条目
|
||||
# 但如果卸载成功,fstab 条目也需要移除。
|
||||
# 如果卸载失败,且 fstab 移除成功,用户需要手动处理。
|
||||
# 更好的做法可能是:卸载成功后,再移除 fstab 条目。
|
||||
# 但为了简化逻辑,这里先移除,并假设卸载会成功。
|
||||
# 实际上,_remove_fstab_entry 应该在卸载成功后才调用,或者在删除分区/格式化时调用。
|
||||
# 对于单纯的卸载操作,不应该自动移除 fstab 条目,除非用户明确要求。
|
||||
# 考虑到当前场景是“卸载后刷新”,通常用户只是想临时卸载,fstab条目不应被删除。
|
||||
# 所以这里暂时注释掉,或者只在删除分区/格式化时调用。
|
||||
# self._remove_fstab_entry(device_path)
|
||||
|
||||
# 执行卸载命令
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"卸载设备 {device_path} 时发生错误: {e}")
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {e}")
|
||||
return False
|
||||
|
||||
def _add_to_fstab(self, device_path, mount_point):
|
||||
"""
|
||||
将设备的挂载信息添加到 /etc/fstab。
|
||||
使用 UUID 方式添加。
|
||||
"""
|
||||
if not isinstance(device_path, str) or not isinstance(mount_point, str):
|
||||
logger.error(f"尝试添加到 fstab 时传入无效的设备路径或挂载点。设备: {device_path} (类型: {type(device_path)}), 挂载点: {mount_point} (类型: {type(mount_point)})")
|
||||
QMessageBox.critical(None, "错误", "无效的设备路径或挂载点,无法添加到 fstab。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试将设备 {device_path} (挂载点: {mount_point}) 添加到 /etc/fstab。")
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
uuid = device_details.get('uuid') if device_details else None
|
||||
fstype = device_details.get('fstype') if device_details else None
|
||||
|
||||
if not uuid or not fstype:
|
||||
logger.error(f"无法获取设备 {device_path} 的 UUID 或文件系统类型,无法添加到 fstab。UUID: {uuid}, FSTYPE: {fstype}")
|
||||
QMessageBox.critical(None, "错误", f"无法获取设备 {device_path} 的 UUID 或文件系统类型,无法添加到 /etc/fstab。")
|
||||
return False
|
||||
|
||||
fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2"
|
||||
fstab_path = self._get_fstab_path()
|
||||
|
||||
try:
|
||||
# 检查是否已存在相同的挂载点或 UUID 条目
|
||||
with open(fstab_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line in lines:
|
||||
if f"UUID={uuid}" in line or mount_point in line.split():
|
||||
logger.warning(f"fstab 中已存在设备 {device_path} (UUID: {uuid}) 或挂载点 {mount_point} 的条目,跳过添加。")
|
||||
return True # 已经存在,认为成功
|
||||
|
||||
# 添加新条目
|
||||
with open(fstab_path, 'a') as f:
|
||||
f.write(fstab_entry + '\n')
|
||||
logger.info(f"成功将 '{fstab_entry}' 添加到 {fstab_path}。")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"将设备 {device_path} 添加到 /etc/fstab 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"将设备 {device_path} 添加到 /etc/fstab 失败。\n错误详情: {e}")
|
||||
return False
|
||||
|
||||
def _remove_fstab_entry(self, device_path):
|
||||
"""
|
||||
从 /etc/fstab 中移除指定设备的条目。
|
||||
:param device_path: 设备路径,例如 /dev/sdb1 或 /dev/md0。
|
||||
:return: True 如果成功移除或没有找到条目,否则 False。
|
||||
"""
|
||||
if not isinstance(device_path, str):
|
||||
logger.warning(f"尝试移除 fstab 条目时传入非字符串设备路径: {device_path} (类型: {type(device_path)})")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试从 /etc/fstab 移除与 {device_path} 相关的条目。")
|
||||
fstab_path = self._get_fstab_path()
|
||||
try:
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
uuid = device_details.get('uuid') if device_details else None
|
||||
|
||||
logger.debug(f"尝试移除 fstab 条目,获取到的设备 {device_path} 的 UUID: {uuid}")
|
||||
|
||||
# 如果没有 UUID 且不是 /dev/ 路径,则无法可靠地从 fstab 中移除
|
||||
if not uuid and not device_path.startswith('/dev/'):
|
||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,也无法识别为 /dev/ 路径,跳过 fstab 移除。")
|
||||
return False
|
||||
|
||||
with open(fstab_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
removed_count = 0
|
||||
for line in lines:
|
||||
# 检查是否是注释或空行
|
||||
if line.strip().startswith('#') or not line.strip():
|
||||
new_lines.append(line)
|
||||
continue
|
||||
|
||||
is_match = False
|
||||
# 尝试匹配 UUID
|
||||
if uuid:
|
||||
if f"UUID={uuid}" in line:
|
||||
is_match = True
|
||||
# 尝试匹配设备路径 (如果 UUID 不存在或不匹配)
|
||||
if not is_match and device_path:
|
||||
# 分割行以避免部分匹配,例如 /dev/sda 匹配 /dev/sda1
|
||||
parts = line.split()
|
||||
if len(parts) > 0 and parts[0] == device_path:
|
||||
is_match = True
|
||||
|
||||
if is_match:
|
||||
logger.info(f"从 {fstab_path} 移除了条目: {line.strip()}")
|
||||
removed_count += 1
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if removed_count > 0:
|
||||
with open(fstab_path, 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
logger.info(f"成功从 {fstab_path} 移除了 {removed_count} 个条目。")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"在 {fstab_path} 中未找到与 {device_path} 相关的条目。")
|
||||
return True # 没有找到也算成功,因为目标是确保没有该条目
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"fstab 文件 {fstab_path} 不存在。")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"从 /etc/fstab 移除条目时发生错误: {e}")
|
||||
QMessageBox.critical(None, "错误", f"从 /etc/fstab 移除条目失败。\n错误详情: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -171,7 +171,6 @@ class LvmOperations:
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除卷组: {vg_name}。")
|
||||
# -y 自动确认, -f 强制删除所有逻辑卷
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["vgremove", "-y", "-f", vg_name],
|
||||
f"删除卷组 {vg_name} 失败"
|
||||
@@ -200,7 +199,6 @@ class LvmOperations:
|
||||
QMessageBox.critical(None, "错误", "逻辑卷大小必须大于0。")
|
||||
return False
|
||||
|
||||
# Confirmation message
|
||||
confirm_message = f"您确定要在卷组 {vg_name} 中创建逻辑卷 {lv_name} 吗?"
|
||||
if use_max_space:
|
||||
confirm_message += "使用卷组所有可用空间。"
|
||||
@@ -215,10 +213,10 @@ class LvmOperations:
|
||||
return False
|
||||
|
||||
if use_max_space:
|
||||
create_cmd = ["lvcreate", "-l", "100%FREE", "-n", lv_name, vg_name]
|
||||
create_cmd = ["lvcreate", "-y", "-l", "100%FREE", "-n", lv_name, vg_name]
|
||||
logger.info(f"尝试在卷组 {vg_name} 中使用最大可用空间创建逻辑卷 {lv_name}。")
|
||||
else:
|
||||
create_cmd = ["lvcreate", "-L", f"{size_gb}G", "-n", lv_name, vg_name]
|
||||
create_cmd = ["lvcreate", "-y", "-L", f"{size_gb}G", "-n", lv_name, vg_name]
|
||||
logger.info(f"尝试在卷组 {vg_name} 中创建 {size_gb}GB 的逻辑卷 {lv_name}。")
|
||||
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
@@ -251,7 +249,6 @@ class LvmOperations:
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除逻辑卷: {vg_name}/{lv_name}。")
|
||||
# -y 自动确认
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["lvremove", "-y", f"{vg_name}/{lv_name}"],
|
||||
f"删除逻辑卷 {vg_name}/{lv_name} 失败"
|
||||
|
||||
239
mainwindow.py
239
mainwindow.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user