300 lines
15 KiB
Python
300 lines
15 KiB
Python
# 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
|