添加解除占用功能
This commit is contained in:
@@ -1,21 +1,18 @@
|
||||
# raid_operations.py
|
||||
import logging
|
||||
import subprocess
|
||||
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit # 导入 QInputDialog 和 QLineEdit
|
||||
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit
|
||||
from PySide6.QtCore import Qt
|
||||
from system_info import SystemInfoManager
|
||||
import re
|
||||
import pexpect # <--- 新增导入 pexpect
|
||||
import pexpect
|
||||
import sys
|
||||
# NEW: 导入 DiskOperations
|
||||
from disk_operations import DiskOperations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RaidOperations:
|
||||
def __init__(self, system_manager: SystemInfoManager, disk_ops: DiskOperations): # <--- Add system_manager and disk_ops parameters
|
||||
def __init__(self, system_manager: SystemInfoManager):
|
||||
self.system_manager = system_manager
|
||||
self.disk_ops = disk_ops # Store disk_ops instance
|
||||
|
||||
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
|
||||
"""
|
||||
@@ -35,10 +32,9 @@ class RaidOperations:
|
||||
logger.info(f"执行命令: {full_cmd_str}")
|
||||
|
||||
try:
|
||||
# SystemInfoManager._run_command 应该负责在 root_privilege=True 时添加 sudo
|
||||
stdout, stderr = self.system_manager._run_command(
|
||||
command_list,
|
||||
root_privilege=True, # 假设所有 RAID 操作都需要 root 权限
|
||||
root_privilege=True,
|
||||
check_output=True,
|
||||
input_data=input_to_command
|
||||
)
|
||||
@@ -54,7 +50,6 @@ class RaidOperations:
|
||||
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):
|
||||
@@ -64,13 +59,12 @@ class RaidOperations:
|
||||
for pattern in suppress_critical_dialog_on_stderr_match:
|
||||
if pattern in stderr_output:
|
||||
should_suppress_dialog = True
|
||||
break # 找到一个匹配就足够了
|
||||
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:
|
||||
@@ -87,25 +81,19 @@ class RaidOperations:
|
||||
专门处理 mdadm --create 命令的交互式执行。
|
||||
通过 pexpect 监听提示并弹出 QMessageBox 让用户选择。
|
||||
"""
|
||||
# pexpect.spawn 需要一个字符串命令,并且 sudo 应该包含在内
|
||||
# 假设 SystemInfoManager._run_command 在 root_privilege=True 时会添加 sudo
|
||||
# 但 pexpect.spawn 需要完整的命令字符串,所以这里手动添加 sudo
|
||||
full_cmd_str = "sudo " + ' '.join(command_list)
|
||||
logger.info(f"执行交互式命令: {full_cmd_str}")
|
||||
|
||||
stdout_buffer = []
|
||||
|
||||
try:
|
||||
# 增加 timeout,因为用户交互可能需要时间
|
||||
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300) # 增加超时到 5 分钟
|
||||
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300)
|
||||
child.logfile = sys.stdout
|
||||
|
||||
# --- 1. 优先处理 sudo 密码提示 ---
|
||||
try:
|
||||
# 尝试匹配 sudo 密码提示,设置较短的超时,如果没出现就继续
|
||||
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
|
||||
if index == 0: # 匹配到密码提示
|
||||
stdout_buffer.append(child.before) # 捕获提示前的输出
|
||||
if index == 0:
|
||||
stdout_buffer.append(child.before)
|
||||
password, ok = QInputDialog.getText(
|
||||
None, "Sudo 密码", "请输入您的 sudo 密码:", QLineEdit.Password
|
||||
)
|
||||
@@ -114,45 +102,36 @@ class RaidOperations:
|
||||
child.close()
|
||||
return False, "".join(stdout_buffer), "Sudo 密码未提供或取消。"
|
||||
child.sendline(password)
|
||||
stdout_buffer.append(child.after) # 捕获发送密码后的输出
|
||||
stdout_buffer.append(child.after)
|
||||
logger.info("已发送 sudo 密码。")
|
||||
elif index == 1: # 超时,未出现密码提示 (可能是 NOPASSWD 或已缓存)
|
||||
elif index == 1:
|
||||
logger.info("Sudo 未提示输入密码(可能已配置 NOPASSWD 或密码已缓存)。")
|
||||
stdout_buffer.append(child.before) # 捕获任何初始输出
|
||||
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 = [ # 重命名为 prompts_data 以避免与循环变量混淆
|
||||
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 警告:检测到驱动器大小不一致或分区表。创建阵列将覆盖数据。是否仍要继续创建阵列?")
|
||||
]
|
||||
|
||||
# 提取所有模式,以便 pexpect.expect 可以同时监听它们
|
||||
patterns_to_expect = [p for p, _ in prompts_data]
|
||||
|
||||
# 循环处理所有可能的提示,直到没有更多提示或命令结束
|
||||
while True:
|
||||
try:
|
||||
# 尝试匹配任何一个预定义的提示
|
||||
# pexpect 会等待直到匹配到其中一个模式,或者超时,或者遇到 EOF
|
||||
index = child.expect(patterns_to_expect, timeout=10) # 每次等待10秒
|
||||
stdout_buffer.append(child.before) # 捕获到提示前的输出
|
||||
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)
|
||||
|
||||
@@ -164,28 +143,20 @@ class RaidOperations:
|
||||
logger.info(f"用户对提示 '{matched_description}' 选择 '否'")
|
||||
|
||||
child.sendline(user_choice)
|
||||
stdout_buffer.append(user_choice + '\n') # 记录用户输入
|
||||
|
||||
# 等待一小段时间,确保 mdadm 接收并处理了输入,并清空缓冲区
|
||||
# 避免在下一次 expect 之前,旧的输出再次被匹配
|
||||
# child.expect(pexpect.TIMEOUT, timeout=1) # This line might consume output needed for next prompt
|
||||
stdout_buffer.append(user_choice + '\n')
|
||||
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
# 如果在等待任何提示时超时,说明没有更多提示了,或者命令已完成。
|
||||
logger.debug("在等待 mdadm 提示时超时,可能没有更多交互。")
|
||||
stdout_buffer.append(child.before) # 捕获当前为止的输出
|
||||
break # 跳出 while 循环,进入等待 EOF 阶段
|
||||
stdout_buffer.append(child.before)
|
||||
break
|
||||
except pexpect.exceptions.EOF:
|
||||
# 如果在等待提示时遇到 EOF,说明命令已经执行完毕或失败
|
||||
logger.warning("在等待 mdadm 提示时遇到 EOF。")
|
||||
stdout_buffer.append(child.before)
|
||||
break # 跳出 while 循环,进入等待 EOF 阶段
|
||||
|
||||
break
|
||||
|
||||
try:
|
||||
# 增加一个合理的超时时间,以防 mdadm 在后台进行长时间初始化
|
||||
child.expect(pexpect.EOF, timeout=120) # 例如,等待 120 秒
|
||||
stdout_buffer.append(child.before) # 捕获命令结束前的任何剩余输出
|
||||
child.expect(pexpect.EOF, timeout=120)
|
||||
stdout_buffer.append(child.before)
|
||||
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
stdout_buffer.append(child.before)
|
||||
@@ -197,12 +168,11 @@ class RaidOperations:
|
||||
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, "" # pexpect 很难区分 stdout/stderr,都视为 stdout
|
||||
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}")
|
||||
@@ -247,7 +217,6 @@ class RaidOperations:
|
||||
: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):
|
||||
@@ -259,13 +228,11 @@ class RaidOperations:
|
||||
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
|
||||
return False
|
||||
level = level_str
|
||||
# --- 标准化 RAID 级别输入结束 ---
|
||||
|
||||
if not devices or len(devices) < 1: # RAID0 理论上可以一个设备,但通常至少两个
|
||||
if not devices or len(devices) < 1:
|
||||
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要一个设备(RAID0)。")
|
||||
return False
|
||||
|
||||
# 检查其他 RAID 级别所需的设备数量
|
||||
if level == "raid1" and len(devices) < 2:
|
||||
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
|
||||
return False
|
||||
@@ -275,17 +242,10 @@ class RaidOperations:
|
||||
if level == "raid6" and len(devices) < 4:
|
||||
QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。")
|
||||
return False
|
||||
if level == "raid10" and len(devices) < 2: # RAID10 至少需要 2 个设备 (例如 1+0)
|
||||
if level == "raid10" and len(devices) < 2:
|
||||
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
|
||||
return False
|
||||
|
||||
# NEW: 尝试解决所有成员设备的占用问题
|
||||
for dev in devices:
|
||||
if not self.disk_ops._resolve_device_occupation(dev, action_description=f"创建 RAID 阵列,成员 {dev}"):
|
||||
logger.info(f"用户取消或未能解决设备 {dev} 的占用问题,取消创建 RAID 阵列。")
|
||||
return False
|
||||
|
||||
# 确认操作
|
||||
reply = QMessageBox.question(None, "确认创建 RAID 阵列",
|
||||
f"你确定要使用设备 {', '.join(devices)} 创建 RAID {level} 阵列吗?\n"
|
||||
"此操作将销毁设备上的所有数据!",
|
||||
@@ -322,23 +282,18 @@ class RaidOperations:
|
||||
create_cmd_base = ["mdadm", "--create", array_name, "--level=" + level.replace("raid", ""),
|
||||
f"--raid-devices={len(devices)}"]
|
||||
|
||||
# 添加 chunk size (RAID0, RAID5, RAID6, RAID10 通常需要)
|
||||
if level in ["raid0", "raid5", "raid6", "raid10"]:
|
||||
create_cmd_base.append(f"--chunk={chunk_size}K")
|
||||
|
||||
create_cmd = create_cmd_base + devices
|
||||
|
||||
# 在 --create 命令中添加 --force 选项
|
||||
create_cmd.insert(2, "--force")
|
||||
|
||||
# 调用新的交互式方法
|
||||
success_create, stdout_create, stderr_create = self._execute_interactive_mdadm_command(
|
||||
create_cmd,
|
||||
f"创建 RAID 阵列失败"
|
||||
)
|
||||
|
||||
if not success_create:
|
||||
# pexpect 捕获的输出可能都在 stdout_create 中
|
||||
if "Array name" in stdout_create and "is in use already" in stdout_create:
|
||||
QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
|
||||
return False
|
||||
@@ -350,7 +305,6 @@ class RaidOperations:
|
||||
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
|
||||
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
|
||||
if success_scan:
|
||||
# --- 新增:确保 /etc 目录存在 ---
|
||||
mkdir_cmd = ["mkdir", "-p", "/etc"]
|
||||
success_mkdir, _, stderr_mkdir = self._execute_shell_command(
|
||||
mkdir_cmd,
|
||||
@@ -359,13 +313,9 @@ class RaidOperations:
|
||||
if not success_mkdir:
|
||||
logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
|
||||
QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
||||
return False # 如果连目录都无法创建,则认为创建阵列失败,因为无法保证重启后自动识别
|
||||
return False
|
||||
logger.info("已确保 /etc 目录存在。")
|
||||
# --- 新增结束 ---
|
||||
|
||||
# 这里需要确保 /etc/mdadm.conf 存在且可写入
|
||||
# _execute_shell_command 会自动添加 sudo
|
||||
# 使用 tee -a 而不是 >> 来确保 sudo 权限下的写入
|
||||
success_append, _, _ = self._execute_shell_command(
|
||||
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
|
||||
"更新 /etc/mdadm/mdadm.conf 失败"
|
||||
@@ -395,11 +345,12 @@ class RaidOperations:
|
||||
|
||||
logger.info(f"尝试停止 RAID 阵列: {array_path}")
|
||||
|
||||
# NEW: 尝试卸载阵列(如果已挂载),利用 DiskOperations 的增强卸载功能
|
||||
# unmount_partition 内部会尝试解决设备占用问题
|
||||
if not self.disk_ops.unmount_partition(array_path, show_dialog_on_error=False):
|
||||
logger.warning(f"未能成功卸载 RAID 阵列 {array_path}。尝试继续停止操作。")
|
||||
# 即使卸载失败,我们仍然尝试停止 mdadm 阵列,因为有时 mdadm --stop 可以在设备忙时强制停止。
|
||||
# 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
|
||||
@@ -408,7 +359,7 @@ class RaidOperations:
|
||||
QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
|
||||
def delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
|
||||
def delete_active_raid_array(self, array_path, member_devices, uuid):
|
||||
"""
|
||||
删除一个活动的 RAID 阵列。
|
||||
此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。
|
||||
@@ -427,7 +378,6 @@ class RaidOperations:
|
||||
logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})")
|
||||
|
||||
# 1. 停止阵列
|
||||
# stop_raid_array 内部会调用 self.disk_ops.unmount_partition 来尝试卸载并解决占用
|
||||
if not self.stop_raid_array(array_path):
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
|
||||
return False
|
||||
@@ -459,7 +409,7 @@ class RaidOperations:
|
||||
except Exception as e:
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||
logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||
return False # 如果配置文件删除失败,也视为整体操作失败
|
||||
return False
|
||||
|
||||
if success_all_cleared:
|
||||
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
|
||||
@@ -469,7 +419,7 @@ class RaidOperations:
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
|
||||
return False
|
||||
|
||||
def delete_configured_raid_array(self, uuid): # <--- 新增方法
|
||||
def delete_configured_raid_array(self, uuid):
|
||||
"""
|
||||
只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。
|
||||
:param uuid: 要删除的 RAID 阵列的 UUID。
|
||||
@@ -494,7 +444,7 @@ class RaidOperations:
|
||||
return False
|
||||
|
||||
|
||||
def activate_raid_array(self, array_path, array_uuid): # 添加 array_uuid 参数
|
||||
def activate_raid_array(self, array_path, array_uuid):
|
||||
"""
|
||||
激活一个已停止的 RAID 阵列。
|
||||
使用 mdadm --assemble <device> --uuid=<uuid>
|
||||
@@ -506,12 +456,9 @@ class RaidOperations:
|
||||
|
||||
logger.info(f"尝试激活 RAID 阵列: {array_path} (UUID: {array_uuid})")
|
||||
|
||||
# 使用 mdadm --assemble <device> --uuid=<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")
|
||||
)
|
||||
|
||||
@@ -522,4 +469,3 @@ class RaidOperations:
|
||||
else:
|
||||
logger.error(f"激活 RAID 阵列 {array_path} 失败。")
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user