411 lines
20 KiB
Python
411 lines
20 KiB
Python
# 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
|