Files
diskmanager/raid_operations.py
2026-02-02 22:31:02 +08:00

300 lines
15 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.

# raid_operations.py
import logging
import subprocess
from PySide6.QtWidgets import QMessageBox
from system_info import SystemInfoManager
import re # 导入正则表达式模块
logger = logging.getLogger(__name__)
class RaidOperations:
def __init__(self):
self.system_manager = SystemInfoManager()
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None): # <--- 添加 input_to_command 参数
"""
执行一个shell命令并返回stdout和stderr。
这个方法包装了 SystemInfoManager._run_command并统一处理日志和错误消息框。
:param command_list: 命令及其参数的列表。
:param error_msg_prefix: 命令失败时显示给用户的错误消息前缀。
:param suppress_critical_dialog_on_stderr_match: 一个字符串,如果在 stderr 中找到此字符串,
则在 CalledProcessError 发生时,只记录日志,不弹出 QMessagebox.critical。
:param input_to_command: 传递给命令stdin的数据 (str)。
:return: (bool success, str stdout, str stderr)
"""
full_cmd_str = ' '.join(command_list)
logger.info(f"执行命令: {full_cmd_str}")
try:
stdout, stderr = self.system_manager._run_command(
command_list,
root_privilege=True,
check_output=True, # RAID操作通常需要检查输出
input_data=input_to_command # <--- 将 input_to_command 传递给 _run_command
)
if stdout: logger.debug(f"命令 '{full_cmd_str}' 标准输出:\n{stdout.strip()}")
if stderr: logger.debug(f"命令 '{full_cmd_str}' 标准错误:\n{stderr.strip()}")
return True, stdout, stderr
except subprocess.CalledProcessError as e:
stderr_output = e.stderr.strip() # 获取标准错误输出
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip()}")
logger.error(f"标准错误: {stderr_output}")
# 根据 suppress_critical_dialog_on_stderr_match 参数决定是否弹出错误对话框
if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in stderr_output:
logger.info(f"特定错误 '{suppress_critical_dialog_on_stderr_match}' 匹配,已抑制错误对话框。")
else:
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
return False, e.stdout, stderr_output # 返回 stderr_output
except FileNotFoundError:
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
return False, "", "Command not found."
except Exception as e:
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str} 发生未知错误: {e}")
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n未知错误: {e}")
return False, "", str(e)
def _get_next_available_md_device_name(self):
"""
查找下一个可用的 /dev/mdX 设备名称(例如 /dev/md0, /dev/md1, ...)。
"""
existing_md_numbers = set()
try:
raid_arrays = self.system_manager.get_mdadm_arrays()
for array in raid_arrays:
device_path = array.get('device')
if device_path and device_path.startswith('/dev/md'):
# 匹配 /dev/mdX 形式的设备名
match = re.match(r'/dev/md(\d+)', device_path)
if match:
existing_md_numbers.add(int(match.group(1)))
except Exception as e:
logger.warning(f"获取现有 RAID 阵列信息失败,可能无法找到最优的下一个设备名: {e}")
next_md_num = 0
while next_md_num in existing_md_numbers:
next_md_num += 1
return f"/dev/md{next_md_num}"
def create_raid_array(self, devices, level, chunk_size):
"""
创建 RAID 阵列。
:param devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
:param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' (也可以是整数 0, 1, 5)
:param chunk_size: Chunk 大小 (KB)
"""
# --- 标准化 RAID 级别输入 ---
if isinstance(level, int):
level_str = f"raid{level}"
elif isinstance(level, str):
if level in ["0", "1", "5", "6", "10"]: # 增加 "6", "10" 确保全面
level_str = f"raid{level}"
else:
level_str = level
else:
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
return False
level = level_str
# --- 标准化 RAID 级别输入结束 ---
if not devices or len(devices) < 2:
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要两个成员设备。")
return False
# 检查 RAID 5 至少需要 3 个设备
if level == "raid5" and len(devices) < 3:
QMessageBox.critical(None, "错误", "RAID5 至少需要三个设备。")
return False
# 补充其他 RAID 级别设备数量检查,与 dialogs.py 保持一致
if level == "raid1" and len(devices) < 2:
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
return False
if level == "raid6" and len(devices) < 4:
QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。")
return False
if level == "raid10" and len(devices) < 2:
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
return False
# 确认操作
reply = QMessageBox.question(None, "确认创建 RAID 阵列",
f"你确定要使用设备 {', '.join(devices)} 创建 RAID {level} 阵列吗?\n"
"此操作将销毁设备上的所有数据!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.No:
logger.info("用户取消了创建 RAID 阵列的操作。")
return False
logger.info(f"尝试创建 RAID {level} 阵列,成员设备: {', '.join(devices)}, Chunk 大小: {chunk_size}KB。")
# 1. 清除设备上的旧 RAID 超级块(如果有)
for dev in devices:
# 使用 --force 选项,避免交互式提示
clear_cmd = ["mdadm", "--zero-superblock", "--force", dev]
# 定义表示设备上没有超级块的错误信息,根据日志调整
no_superblock_error_match = "Unrecognised md component device"
success, _, stderr = self._execute_shell_command(
clear_cmd,
f"清除设备 {dev} 上的旧 RAID 超级块失败",
suppress_critical_dialog_on_stderr_match=no_superblock_error_match
)
# 检查 stderr 是否包含“未识别的 MD 组件设备”信息
if no_superblock_error_match in stderr:
logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。")
elif success:
logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。")
else:
logger.error(f"清除设备 {dev} 上的旧 RAID 超级块失败,中断 RAID 创建。")
return False
# 2. 创建 RAID 阵列
# --- 修改点:自动生成唯一的阵列名称 ---
array_name = self._get_next_available_md_device_name()
logger.info(f"将使用自动生成的 RAID 阵列名称: {array_name}")
# --- 修改点结束 ---
if level == "raid0":
create_cmd = ["mdadm", "--create", array_name, "--level=raid0",
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
elif level == "raid1":
create_cmd = ["mdadm", "--create", array_name, "--level=raid1",
f"--raid-devices={len(devices)}"] + devices
elif level == "raid5":
create_cmd = ["mdadm", "--create", array_name, "--level=raid5",
f"--raid-devices={len(devices)}"] + devices
elif level == "raid6": # 增加 RAID6
create_cmd = ["mdadm", "--create", array_name, "--level=raid6",
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
elif level == "raid10": # 增加 RAID10
create_cmd = ["mdadm", "--create", array_name, "--level=raid10",
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
else:
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别: {level}")
return False
# 在 --create 命令中添加 --force 选项
create_cmd.insert(2, "--force")
success_create, stdout_create, stderr_create = self._execute_shell_command(
create_cmd,
f"创建 RAID 阵列失败",
input_to_command='y\n' # 尝试通过 stdin 传递 'y'
)
if not success_create:
# 检查是否是由于 "Array name ... is in use already." 导致的失败
if "Array name" in stderr_create and "is in use already" in stderr_create:
QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
return False
logger.info(f"成功创建 RAID {level} 阵列 {array_name}")
QMessageBox.information(None, "成功", f"成功创建 RAID {level} 阵列 {array_name}")
# 3. 刷新 mdadm 配置并等待阵列激活
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
if success_scan:
append_to_conf_cmd = ["bash", "-c", f"echo '{scan_stdout.strip()}' >> /etc/mdadm/mdadm.conf"]
if not self._execute_shell_command(append_to_conf_cmd, "更新 /etc/mdadm/mdadm.conf 失败")[0]:
logger.warning("更新 /etc/mdadm/mdadm.conf 失败。")
else:
logger.warning("未能扫描到 mdadm 配置,跳过更新 mdadm.conf。")
return True
def stop_raid_array(self, array_path):
"""
停止一个 RAID 阵列。
:param array_path: RAID 阵列的设备路径,例如 /dev/md0
"""
reply = QMessageBox.question(None, "确认停止 RAID 阵列",
f"你确定要停止 RAID 阵列 {array_path} 吗?\n"
"停止阵列将使其无法访问,并可能需要重新组装。",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.No:
logger.info(f"用户取消了停止 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试停止 RAID 阵列: {array_path}")
# 尝试卸载阵列(如果已挂载),不显示错误对话框
# 注意:这里需要调用 disk_operations 的 unmount_partition
# 由于 RaidOperations 不直接持有 DiskOperations 实例,需要通过某种方式获取或传递
# 暂时先直接调用 umount 命令,不处理 fstab
# 或者,更好的方式是让 MainWindow 调用 disk_ops.unmount_partition
# 此处简化处理,只执行 umount 命令
# 这里的 suppress_critical_dialog_on_stderr_match 应该与 DiskOperations 中的定义保持一致
self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败",
suppress_critical_dialog_on_stderr_match="not mounted") # 仅抑制英文,因为这里没有访问 DiskOperations 的 already_unmounted_errors
if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]:
return False
logger.info(f"成功停止 RAID 阵列 {array_path}")
QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}")
return True
def delete_raid_array(self, array_path, member_devices):
"""
删除一个 RAID 阵列。
此操作将停止阵列并清除成员设备上的超级块。
:param array_path: RAID 阵列的设备路径,例如 /dev/md0
:param member_devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
"""
reply = QMessageBox.question(None, "确认删除 RAID 阵列",
f"你确定要删除 RAID 阵列 {array_path} 吗?\n"
"此操作将停止阵列并清除成员设备上的 RAID 超级块,数据将无法访问!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.No:
logger.info(f"用户取消了删除 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试删除 RAID 阵列: {array_path}")
# 1. 停止阵列
if not self.stop_raid_array(array_path):
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
return False
# 2. 清除成员设备上的超级块
success_all_cleared = True
for dev in member_devices:
# 使用 --force 选项,避免交互式提示
clear_cmd = ["mdadm", "--zero-superblock", "--force", dev]
# 定义表示设备上没有超级块的错误信息,根据日志调整
no_superblock_error_match = "Unrecognised md component device"
success, _, stderr = self._execute_shell_command(
clear_cmd,
f"清除设备 {dev} 上的 RAID 超级块失败",
suppress_critical_dialog_on_stderr_match=no_superblock_error_match
)
if no_superblock_error_match in stderr:
logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。")
elif success:
logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。")
else:
success_all_cleared = False
# 错误对话框已由 _execute_shell_command 弹出,这里只记录警告
logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。")
if success_all_cleared:
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
QMessageBox.information(None, "成功", f"成功删除 RAID 阵列 {array_path}")
return True
else:
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
return False