# 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