Files
diskmanager2/raid_operations_tkinter.py
2026-02-09 03:14:35 +08:00

411 lines
20 KiB
Python
Raw Permalink 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_tkinter.py
import logging
import subprocess
from tkinter import messagebox, simpledialog
from system_info import SystemInfoManager
import re
try:
import pexpect
PEXPECT_AVAILABLE = True
except ImportError:
PEXPECT_AVAILABLE = False
pexpect = None
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"""
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:
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
return False, e.stdout, stderr_output
except FileNotFoundError:
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
messagebox.showerror("错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
return False, "", "Command not found."
except Exception as e:
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str} 发生未知错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
return False, "", str(e)
def _execute_interactive_mdadm_command(self, command_list, error_msg_prefix):
"""专门处理 mdadm --create 命令的交互式执行"""
if not PEXPECT_AVAILABLE:
logger.warning("pexpect 模块未安装,将尝试使用 --run 参数避免交互")
# 添加 --run 参数来避免交互式提示(自动回答 yes
if "--create" in command_list:
# 在 --create 后面插入 --run
create_idx = command_list.index("--create")
command_list.insert(create_idx + 1, "--run")
logger.info("已添加 --run 参数以自动确认创建")
# 回退到普通命令执行
return self._execute_shell_command(command_list, error_msg_prefix)
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 = simpledialog.askstring("Sudo 密码", "请输入您的 sudo 密码:", show='*')
if not password:
logger.error("sudo 密码未提供或取消。")
messagebox.showerror("错误", "sudo 密码未提供或取消。")
child.close(force=True)
return False, "", "Sudo password not provided."
child.sendline(password)
try:
auth_index = child.expect(['Authentication failure', pexpect.TIMEOUT], timeout=5)
if auth_index == 0:
logger.error("sudo 密码验证失败。")
messagebox.showerror("错误", "sudo 密码验证失败。请检查密码并重试。")
child.close(force=True)
return False, "", "Sudo authentication failed."
except pexpect.TIMEOUT:
logger.info("sudo 密码验证成功或无需密码。继续...")
except pexpect.TIMEOUT:
logger.info("未在 5 秒内收到密码提示,假定命令无需密码或已在缓存中。继续...")
interaction_patterns = [
r'Continue creating array.*\?',
r'partition table exists.*Continue',
r'write-indent bitmap.*enable it now.*\?',
r'bitmap.*\[y/N\]\?',
]
pattern_descriptions = {
0: "mdadm 询问是否继续创建阵列(分区表将被清除)",
1: "mdadm 提示分区表存在,询问是否继续",
2: "mdadm 询问是否启用 write-intent bitmap",
3: "mdadm 询问是否启用 bitmap",
}
while True:
try:
index = child.expect(interaction_patterns + [pexpect.EOF, pexpect.TIMEOUT], timeout=300)
if index < len(interaction_patterns):
# 匹配到交互式提示
matched_text = child.after
logger.info(f"检测到交互式提示 (index={index}): {matched_text}")
# 根据不同的提示给出不同的处理
if index == 0 or index == 1:
# 继续创建阵列(分区表相关)
if messagebox.askyesno("mdadm 提示",
"设备上存在分区表,创建 RAID 将清除分区表。\n"
"是否继续创建 RAID 阵列?",
default=messagebox.YES):
child.sendline('y')
logger.info("用户确认继续创建 RAID 阵列。")
else:
child.sendline('n')
logger.info("用户选择不继续创建 RAID 阵列。中止操作。")
return False, "", "User aborted RAID creation."
elif index == 2 or index == 3:
# bitmap 相关提示
if messagebox.askyesno("mdadm 提示",
"mdadm 建议启用 write-intent bitmap 以优化恢复速度。\n"
"是否启用 bitmap",
default=messagebox.NO):
child.sendline('y')
logger.info("用户确认启用 bitmap。")
else:
child.sendline('n')
logger.info("用户选择不启用 bitmap继续创建阵列。")
elif index == len(interaction_patterns):
# EOF
break
elif index == len(interaction_patterns) + 1:
# TIMEOUT
logger.warning(f"等待 mdadm 命令输出时超时。已收集输出:\n{''.join(stdout_buffer)}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: mdadm 命令在交互完成后未正常退出,等待 EOF 超时。")
return False, "".join(stdout_buffer), "Timeout waiting for EOF."
except pexpect.exceptions.EOF:
break
final_output = child.before
stdout_buffer.append(final_output)
child.close()
if child.exitstatus == 0:
logger.info(f"交互式命令成功完成: {full_cmd_str}")
return True, "".join(stdout_buffer), ""
else:
logger.error(f"交互式命令失败,退出码: {child.exitstatus}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
return False, "".join(stdout_buffer), f"Command failed with exit code {child.exitstatus}"
except pexpect.exceptions.ExceptionPexpect as e:
logger.error(f"交互式命令执行过程中发生 pexpect 错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: pexpect 内部错误: {e}")
return False, "", str(e)
except Exception as e:
logger.error(f"交互式命令执行时发生未知错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
return False, "", str(e)
def create_raid_array(self, devices, level, chunk_size):
"""创建 RAID 阵列"""
if not isinstance(devices, list):
logger.error(f"传入的设备列表类型不是 list: {type(devices)}")
messagebox.showerror("错误", f"不支持的 RAID 级别类型: {type(level)}")
return False
if not devices:
messagebox.showerror("错误", "创建 RAID 阵列至少需要一个设备RAID0")
return False
num_devices = len(devices)
if level == "1" and num_devices < 2:
messagebox.showerror("错误", "RAID1 至少需要两个设备。")
return False
elif level == "5" and num_devices < 3:
messagebox.showerror("错误", "RAID5 至少需要三个设备。")
return False
elif level == "6" and num_devices < 4:
messagebox.showerror("错误", "RAID6 至少需要四个设备。")
return False
elif level == "10" and num_devices < 2:
messagebox.showerror("错误", "RAID10 至少需要两个设备。")
return False
if not messagebox.askyesno("确认创建 RAID 阵列",
f"您确定要创建 RAID {level} 阵列吗?\n"
f"这将使用设备: {', '.join(devices)}\n"
f"此操作会擦除这些设备上的数据!",
default=messagebox.NO):
logger.info("用户取消了创建 RAID 阵列的操作。")
return False
array_name = None
try:
mdadm_detail_result = self.system_manager._run_command(["mdadm", "--detail", "--scan"], root_privilege=True)
existing_arrays = re.findall(r'ARRAY\s+/dev/(md\d+)', mdadm_detail_result[0])
if existing_arrays:
max_num = max([int(re.search(r'\d+', name).group()) for name in existing_arrays])
array_name = f"/dev/md{max_num + 1}"
else:
array_name = "/dev/md0"
except Exception as e:
logger.warning(f"检测现有 RAID 阵列时出错: {e},将默认使用 /dev/md0。")
array_name = "/dev/md0"
create_cmd = [
"mdadm", "--create", array_name, "--level=" + level,
"--raid-devices=" + str(num_devices)
]
if chunk_size:
create_cmd.append("--chunk=" + str(chunk_size))
create_cmd.extend(devices)
logger.info(f"尝试创建 RAID {level} 阵列 {array_name},使用设备: {devices}")
success, stdout, stderr = self._execute_interactive_mdadm_command(create_cmd, f"创建 RAID {level} 阵列失败")
if success:
if "already exists" in stderr.lower() or "already exists" in stdout.lower():
messagebox.showerror("错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
return False
messagebox.showinfo("成功", f"成功创建 RAID {level} 阵列 {array_name}")
try:
mdadm_conf_path = "/etc/mdadm.conf"
success_scan, stdout_scan, stderr_scan = self._execute_shell_command(
["mdadm", "--detail", "--scan"],
"生成 mdadm.conf 扫描信息失败",
suppress_critical_dialog_on_stderr_match="No arrays"
)
if success_scan:
success_mkdir, _, _ = self._execute_shell_command(
["mkdir", "-p", "/etc"],
"创建 /etc 目录失败"
)
if not success_mkdir:
messagebox.showerror("错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
return False
success_conf, _, stderr_conf = self._execute_shell_command(
["sh", "-c", f"echo '{stdout_scan.strip()}' >> {mdadm_conf_path}"],
f"更新 {mdadm_conf_path} 失败"
)
if success_conf:
logger.info(f"已将新的 RAID 阵列信息追加到 {mdadm_conf_path}")
else:
logger.warning(f"无法追加到 {mdadm_conf_path}。这可能需要手动修复。错误: {stderr_conf}")
else:
logger.warning("mdadm --detail --scan 失败,无法自动更新 mdadm.conf。")
except Exception as e:
logger.error(f"更新 mdadm.conf 时发生错误: {e}")
return True
return False
def stop_raid_array(self, array_path):
"""停止 RAID 阵列"""
if not messagebox.askyesno("确认停止 RAID 阵列",
f"您确定要停止 RAID 阵列 {array_path} 吗?\n"
f"这将卸载该阵列(如果已挂载)并将其置为停止状态。",
default=messagebox.NO):
logger.info(f"用户取消了停止 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试停止 RAID 阵列 {array_path}")
try:
stdout, stderr = self.system_manager._run_command(
["mdadm", "--stop", array_path],
root_privilege=True, check_output=True
)
messagebox.showinfo("成功", f"成功停止 RAID 阵列 {array_path}")
return True
except subprocess.CalledProcessError as e:
error_msg = e.stderr.strip() if e.stderr else str(e)
logger.error(f"停止 RAID 阵列 {array_path} 失败: {error_msg}")
messagebox.showerror("错误", f"停止 RAID 阵列 {array_path} 失败。\n错误: {error_msg}")
return False
def delete_active_raid_array(self, array_path, member_devices, array_uuid):
"""删除活动的 RAID 阵列"""
if not messagebox.askyesno("确认删除 RAID 阵列",
f"您确定要删除 RAID 阵列 {array_path} 吗?\n"
f"此操作将停止阵列并清除所有成员设备的超级块!",
default=messagebox.NO):
logger.info(f"用户取消了删除 RAID 阵列 {array_path} 的操作。")
return False
if not self.stop_raid_array(array_path):
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
return False
all_wiped_successfully = True
if member_devices:
for dev in member_devices:
if dev:
logger.info(f"尝试清除设备 {dev} 上的 RAID 超级块。")
wipe_success, _, wipe_stderr = self._execute_shell_command(
["mdadm", "--zero-superblock", dev],
f"清除设备 {dev} 的 RAID 超级块失败"
)
if not wipe_success:
logger.error(f"清除设备 {dev} 的 RAID 超级块失败: {wipe_stderr}")
all_wiped_successfully = False
try:
mdadm_conf_path = "/etc/mdadm.conf"
if array_uuid:
success_conf_rm, _, stderr_conf_rm = self._execute_shell_command(
["sed", "-i", f"/{re.escape(array_uuid)}/d", mdadm_conf_path],
f"{mdadm_conf_path} 中删除 RAID 条目失败",
suppress_critical_dialog_on_stderr_match=("No such file", "没有那个文件")
)
if not success_conf_rm:
logger.warning(f"无法从 {mdadm_conf_path} 中删除条目: {stderr_conf_rm}")
except Exception as e:
logger.error(f"更新 {mdadm_conf_path} 时发生错误: {e}")
if all_wiped_successfully:
messagebox.showinfo("成功", f"成功删除 RAID 阵列 {array_path}")
return True
else:
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
return False
def delete_configured_raid_array(self, uuid):
"""删除停止状态 RAID 阵列的配置文件条目"""
if not messagebox.askyesno("确认删除 RAID 阵列配置",
f"您确定要删除 UUID 为 {uuid} 的 RAID 阵列配置吗?\n"
f"这将从 mdadm.conf 中删除该阵列的配置。",
default=messagebox.NO):
logger.info(f"用户取消了删除 RAID 阵列配置 (UUID: {uuid}) 的操作。")
return False
mdadm_conf_path = "/etc/mdadm.conf"
success, _, stderr = self._execute_shell_command(
["sed", "-i", f"/{re.escape(uuid)}/d", mdadm_conf_path],
f"{mdadm_conf_path} 中删除 RAID 条目失败"
)
if success:
messagebox.showinfo("成功", f"成功删除 UUID 为 {uuid} 的 RAID 阵列配置。")
return True
else:
messagebox.showerror("错误", f"删除 RAID 阵列配置失败: {e}")
return False
def activate_raid_array(self, array_path, array_uuid):
"""激活已停止的 RAID 阵列"""
if not array_uuid or array_uuid == 'N/A':
messagebox.showerror("错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
return False
if not messagebox.askyesno("确认激活 RAID 阵列",
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
"这将尝试重新组装阵列。",
default=messagebox.NO):
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试激活 RAID 阵列 {array_path} (UUID: {array_uuid})。")
success, stdout, stderr = self._execute_shell_command(
["mdadm", "--assemble", "--uuid", array_uuid, array_path],
f"激活 RAID 阵列 {array_path} 失败"
)
if success:
messagebox.showinfo("成功", f"成功激活 RAID 阵列 {array_path}")
return True
return False