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

223 lines
12 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:
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip()}")
logger.error(f"标准错误: {e.stderr.strip()}")
# 根据 suppress_critical_dialog_on_stderr_match 参数决定是否弹出错误对话框
if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in e.stderr.strip():
logger.info(f"特定错误 '{suppress_critical_dialog_on_stderr_match}' 匹配,已抑制错误对话框。")
else:
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {e.stderr.strip()}")
return False, e.stdout, e.stderr
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 create_raid_array(self, devices, level, chunk_size):
"""
创建 RAID 阵列。
:param devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
:param level: RAID 级别,例如 'raid0', 'raid1', 'raid5'
:param chunk_size: Chunk 大小 (KB)
"""
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
# 确认操作
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]
success, _, stderr = self._execute_shell_command(
clear_cmd,
f"清除设备 {dev} 上的旧 RAID 超级块失败",
suppress_critical_dialog_on_stderr_match="No superblocks" # 抑制“没有超级块”的错误提示
)
if success:
logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。")
else:
# 如果清除失败,但不是因为“没有超级块”,则可能需要进一步处理
if "No superblocks" not in stderr:
QMessageBox.warning(None, "警告", f"清除设备 {dev} 上的旧 RAID 超级块可能失败,但尝试继续。")
# 2. 创建 RAID 阵列
# 默认阵列名为 /dev/md/new_raid可以考虑让用户输入
array_name = "/dev/md/new_raid"
if level == "raid0":
# RAID0 至少2个设备
create_cmd = ["mdadm", "--create", array_name, "--level=raid0",
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
elif level == "raid1":
# RAID1 至少2个设备
create_cmd = ["mdadm", "--create", array_name, "--level=raid1",
f"--raid-devices={len(devices)}"] + devices
elif level == "raid5":
# RAID5 至少3个设备
create_cmd = ["mdadm", "--create", array_name, "--level=raid5",
f"--raid-devices={len(devices)}"] + devices
else:
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别: {level}")
return False
# 在 --create 命令中添加 --force 选项
create_cmd.insert(2, "--force") # 插入到 --create 后面
# <--- 关键修改:通过 input_to_command 参数传入 'y\n' 来强制 mdadm 接受
if not self._execute_shell_command(create_cmd, f"创建 RAID 阵列失败", input_to_command='y\n')[0]:
return False
logger.info(f"成功创建 RAID {level} 阵列 {array_name}")
QMessageBox.information(None, "成功", f"成功创建 RAID {level} 阵列 {array_name}")
# 3. 刷新 mdadm 配置并等待阵列激活
# 注意:这里使用 '>>' 重定向subprocess.run 无法直接处理 shell 重定向符号
# 需要改成先读取,再写入,或者使用 bash -c "..."
# 暂时先用 bash -c 的方式,更简单
# 旧代码self._execute_shell_command(["mdadm", "--examine", "--scan", ">>", "/etc/mdadm/mdadm.conf"], "更新 mdadm.conf 失败")
# 获取新的 mdadm.conf 内容
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
if success_scan:
# 将扫描结果追加到 mdadm.conf
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。")
# self._execute_shell_command(["update-initramfs", "-u"], "更新 initramfs 失败")
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 命令
self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", suppress_critical_dialog_on_stderr_match="not mounted")
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]
if not self._execute_shell_command(clear_cmd, f"清除设备 {dev} 上的 RAID 超级块失败")[0]:
success_all_cleared = False
QMessageBox.warning(None, "警告", 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