Files
diskmanager/raid_operations.py
2026-02-05 01:41:11 +08:00

472 lines
23 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, QInputDialog, QLineEdit
from PySide6.QtCore import Qt
from system_info import SystemInfoManager
import re
import pexpect
import sys
logger = logging.getLogger(__name__)
class RaidOperations:
def __init__(self, system_manager: SystemInfoManager):
self.system_manager = system_manager
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
"""
执行一个shell命令并返回stdout和stderr。
这个方法包装了 SystemInfoManager._run_command并统一处理日志和错误消息框。
它会通过 SystemInfoManager 自动处理 sudo。
: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,
input_data=input_to_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}")
should_suppress_dialog = False
if suppress_critical_dialog_on_stderr_match:
if isinstance(suppress_critical_dialog_on_stderr_match, str):
if suppress_critical_dialog_on_stderr_match in stderr_output:
should_suppress_dialog = True
elif isinstance(suppress_critical_dialog_on_stderr_match, (list, tuple)):
for pattern in suppress_critical_dialog_on_stderr_match:
if pattern in stderr_output:
should_suppress_dialog = True
break
if should_suppress_dialog:
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
else:
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
return False, e.stdout, 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 _execute_interactive_mdadm_command(self, command_list, error_msg_prefix):
"""
专门处理 mdadm --create 命令的交互式执行。
通过 pexpect 监听提示并弹出 QMessageBox 让用户选择。
"""
full_cmd_str = "sudo " + ' '.join(command_list)
logger.info(f"执行交互式命令: {full_cmd_str}")
stdout_buffer = []
try:
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300)
child.logfile = sys.stdout
try:
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
if index == 0:
stdout_buffer.append(child.before)
password, ok = QInputDialog.getText(
None, "Sudo 密码", "请输入您的 sudo 密码:", QLineEdit.Password
)
if not ok or not password:
QMessageBox.critical(None, "错误", "sudo 密码未提供或取消。")
child.close()
return False, "".join(stdout_buffer), "Sudo 密码未提供或取消。"
child.sendline(password)
stdout_buffer.append(child.after)
logger.info("已发送 sudo 密码。")
elif index == 1:
logger.info("Sudo 未提示输入密码(可能已配置 NOPASSWD 或密码已缓存)。")
stdout_buffer.append(child.before)
except pexpect.exceptions.EOF:
stdout_buffer.append(child.before)
logger.error("命令在 sudo 密码提示前退出。")
child.close()
return False, "".join(stdout_buffer), "命令在 sudo 密码提示前退出。"
except pexpect.exceptions.TIMEOUT:
logger.info("Sudo 密码提示超时,可能已配置 NOPASSWD 或密码已缓存。")
stdout_buffer.append(child.before)
prompts_data = [
(r"To optimalize recovery speed, .* write-(?:intent|indent) bitmap, do you want to enable it now\? \[y/N\]\s*",
"mdadm 建议启用写入意图位图以优化恢复速度。您希望现在启用它吗?"),
(r"Continue creating array \[y/N\]\s*",
"mdadm 警告:检测到驱动器大小不一致或分区表。创建阵列将覆盖数据。是否仍要继续创建阵列?")
]
patterns_to_expect = [p for p, _ in prompts_data]
while True:
try:
index = child.expect(patterns_to_expect, timeout=10)
stdout_buffer.append(child.before)
matched_description = prompts_data[index][1]
reply = QMessageBox.question(None, "mdadm 提示", matched_description,
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if reply == QMessageBox.Yes:
user_choice = 'y'
logger.info(f"用户对提示 '{matched_description}' 选择 ''")
else:
user_choice = 'n'
logger.info(f"用户对提示 '{matched_description}' 选择 ''")
child.sendline(user_choice)
stdout_buffer.append(user_choice + '\n')
except pexpect.exceptions.TIMEOUT:
logger.debug("在等待 mdadm 提示时超时,可能没有更多交互。")
stdout_buffer.append(child.before)
break
except pexpect.exceptions.EOF:
logger.warning("在等待 mdadm 提示时遇到 EOF。")
stdout_buffer.append(child.before)
break
try:
child.expect(pexpect.EOF, timeout=120)
stdout_buffer.append(child.before)
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
except pexpect.exceptions.TIMEOUT:
stdout_buffer.append(child.before)
logger.error(f"mdadm 命令在所有交互完成后,等待 EOF 超时。")
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: mdadm 命令在交互完成后未正常退出,等待 EOF 超时。")
child.close()
return False, "".join(stdout_buffer), "mdadm 命令等待 EOF 超时。"
except pexpect.exceptions.EOF:
stdout_buffer.append(child.before)
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
child.close()
final_output = "".join(stdout_buffer)
if child.exitstatus == 0:
logger.info(f"交互式命令 {full_cmd_str} 成功完成。")
return True, final_output, ""
else:
logger.error(f"交互式命令 {full_cmd_str} 失败,退出码: {child.exitstatus}")
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
return False, final_output, f"命令执行失败,退出码 {child.exitstatus}"
except pexpect.exceptions.ExceptionPexpect as e:
logger.error(f"pexpect 内部错误: {e}")
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: pexpect 内部错误: {e}")
return False, "", str(e)
except Exception as e:
logger.error(f"执行交互式命令 {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'):
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)
"""
if isinstance(level, int):
level_str = f"raid{level}"
elif isinstance(level, str):
if level in ["0", "1", "5", "6", "10"]:
level_str = f"raid{level}"
else:
level_str = level
else:
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
return False
level = level_str
if not devices or len(devices) < 1:
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要一个设备RAID0")
return False
if level == "raid1" and len(devices) < 2:
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
return False
if level == "raid5" and len(devices) < 3:
QMessageBox.critical(None, "错误", "RAID5 至少需要三个设备。")
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:
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:
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}")
create_cmd_base = ["mdadm", "--create", array_name, "--level=" + level.replace("raid", ""),
f"--raid-devices={len(devices)}"]
if level in ["raid0", "raid5", "raid6", "raid10"]:
create_cmd_base.append(f"--chunk={chunk_size}K")
create_cmd = create_cmd_base + devices
create_cmd.insert(2, "--force")
success_create, stdout_create, stderr_create = self._execute_interactive_mdadm_command(
create_cmd,
f"创建 RAID 阵列失败"
)
if not success_create:
if "Array name" in stdout_create and "is in use already" in stdout_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:
mkdir_cmd = ["mkdir", "-p", "/etc"]
success_mkdir, _, stderr_mkdir = self._execute_shell_command(
mkdir_cmd,
"创建 /etc 目录失败"
)
if not success_mkdir:
logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
return False
logger.info("已确保 /etc 目录存在。")
success_append, _, _ = self._execute_shell_command(
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
"更新 /etc/mdadm/mdadm.conf 失败"
)
if not success_append:
logger.warning("更新 /etc/mdadm.conf 失败。")
else:
logger.info("已成功更新 /etc/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}")
# Original umount call (not using disk_ops.unmount_partition)
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_active_raid_array(self, array_path, member_devices, uuid):
"""
删除一个活动的 RAID 阵列。
此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。
:param array_path: RAID 阵列的设备路径,例如 /dev/md0
:param member_devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
:param uuid: RAID 阵列的 UUID
"""
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} (UUID: {uuid})")
# 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:
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
logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。")
# 3. 从 mdadm.conf 中删除配置条目
try:
self.system_manager.delete_raid_array_config(uuid)
logger.info(f"已成功从 mdadm.conf 中删除 UUID 为 {uuid} 的 RAID 阵列配置。")
except Exception as e:
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
return False
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
def delete_configured_raid_array(self, uuid):
"""
只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。
:param uuid: 要删除的 RAID 阵列的 UUID。
:return: True 如果成功False 如果失败。
"""
reply = QMessageBox.question(None, "确认删除 RAID 阵列配置",
f"您确定要删除 UUID 为 {uuid} 的 RAID 阵列配置吗?\n"
"此操作将从配置文件中移除该条目,但不会影响实际的磁盘数据或活动阵列。",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.No:
logger.info(f"用户取消了删除 UUID 为 {uuid} 的 RAID 阵列配置的操作。")
return False
logger.info(f"尝试删除 UUID 为 {uuid} 的 RAID 阵列配置文件条目。")
try:
self.system_manager.delete_raid_array_config(uuid)
QMessageBox.information(None, "成功", f"成功删除 UUID 为 {uuid} 的 RAID 阵列配置。")
return True
except Exception as e:
QMessageBox.critical(None, "错误", f"删除 RAID 阵列配置失败: {e}")
logger.error(f"删除 RAID 阵列配置失败: {e}")
return False
def activate_raid_array(self, array_path, array_uuid):
"""
激活一个已停止的 RAID 阵列。
使用 mdadm --assemble <device> --uuid=<uuid>
"""
if not array_uuid or array_uuid == 'N/A':
QMessageBox.critical(None, "错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
logger.error(f"激活 RAID 阵列 {array_path} 失败:缺少 UUID。")
return False
logger.info(f"尝试激活 RAID 阵列: {array_path} (UUID: {array_uuid})")
success, stdout, stderr = self._execute_shell_command(
["mdadm", "--assemble", array_path, "--uuid", array_uuid],
f"激活 RAID 阵列 {array_path} 失败",
suppress_critical_dialog_on_stderr_match=("mdadm: device /dev/md", "already active", "No such file or directory")
)
if success:
logger.info(f"成功激活 RAID 阵列 {array_path} (UUID: {array_uuid})。")
QMessageBox.information(None, "成功", f"成功激活 RAID 阵列 {array_path}")
return True
else:
logger.error(f"激活 RAID 阵列 {array_path} 失败。")
return False