fix 44
This commit is contained in:
Binary file not shown.
Binary file not shown.
164
mainwindow.py
164
mainwindow.py
@@ -330,7 +330,7 @@ class MainWindow(QMainWindow):
|
||||
self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
||||
|
||||
try:
|
||||
raid_arrays = self.system_manager.get_mdadm_arrays()
|
||||
raid_arrays = self.system_manager.get_mdadm_arrays() # 现在会返回所有阵列,包括停止的
|
||||
if not raid_arrays:
|
||||
item = QTreeWidgetItem(self.ui.treeWidget_raid)
|
||||
item.setText(0, "未找到RAID阵列。")
|
||||
@@ -344,11 +344,14 @@ class MainWindow(QMainWindow):
|
||||
logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。")
|
||||
continue
|
||||
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
# 对于停止状态的阵列,挂载点可能不存在或不相关
|
||||
current_mount_point = ""
|
||||
if array.get('state') != 'Stopped (Configured)':
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
|
||||
array_item.setText(0, array_path)
|
||||
array_item.setText(1, array.get('level', 'N/A'))
|
||||
array_item.setText(2, array.get('state', 'N/A'))
|
||||
array_item.setText(2, array.get('state', 'N/A')) # 显示实际状态
|
||||
array_item.setText(3, array.get('array_size', 'N/A'))
|
||||
array_item.setText(4, array.get('active_devices', 'N/A'))
|
||||
array_item.setText(5, array.get('failed_devices', 'N/A'))
|
||||
@@ -359,16 +362,18 @@ class MainWindow(QMainWindow):
|
||||
array_item.setText(10, array.get('chunk_size', 'N/A'))
|
||||
array_item.setText(11, current_mount_point if current_mount_point else "")
|
||||
array_item.setExpanded(True)
|
||||
array_data_for_context = array.copy()
|
||||
array_data_for_context['device'] = array_path
|
||||
array_item.setData(0, Qt.UserRole, array_data_for_context)
|
||||
|
||||
for member in array.get('member_devices', []):
|
||||
member_item = QTreeWidgetItem(array_item)
|
||||
member_item.setText(0, f" {member.get('device_path', 'N/A')}")
|
||||
member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}")
|
||||
member_item.setText(2, member.get('state', 'N/A'))
|
||||
member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}")
|
||||
# 存储完整的阵列数据,包括状态,供上下文菜单使用
|
||||
array_item.setData(0, Qt.UserRole, array)
|
||||
|
||||
# 对于停止状态的阵列,成员设备可能为空,跳过显示子项
|
||||
if array.get('state') != 'Stopped (Configured)':
|
||||
for member in array.get('member_devices', []):
|
||||
member_item = QTreeWidgetItem(array_item)
|
||||
member_item.setText(0, f" {member.get('device_path', 'N/A')}")
|
||||
member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") # 假设 raid_device 字段存在
|
||||
member_item.setText(2, member.get('state', 'N/A'))
|
||||
# member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}") # 假设 major/minor 字段存在
|
||||
|
||||
for i in range(len(raid_headers)):
|
||||
self.ui.treeWidget_raid.resizeColumnToContents(i)
|
||||
@@ -379,49 +384,95 @@ class MainWindow(QMainWindow):
|
||||
logger.error(f"刷新RAID阵列信息失败: {e}")
|
||||
|
||||
def show_raid_context_menu(self, pos: QPoint):
|
||||
item = self.ui.treeWidget_raid.itemAt(pos)
|
||||
menu = QMenu(self)
|
||||
item = self.ui.treeWidget_raid.itemAt(pos)
|
||||
menu = QMenu(self)
|
||||
|
||||
create_raid_action = menu.addAction("创建 RAID 阵列...")
|
||||
create_raid_action.triggered.connect(self._handle_create_raid_array)
|
||||
menu.addSeparator()
|
||||
|
||||
if item and item.parent() is None:
|
||||
array_data = item.data(0, Qt.UserRole)
|
||||
if not array_data:
|
||||
logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。")
|
||||
return
|
||||
|
||||
array_path = array_data.get('device')
|
||||
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
|
||||
|
||||
if not array_path or array_path == 'N/A':
|
||||
logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。")
|
||||
return
|
||||
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
|
||||
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
|
||||
unmount_action = menu.addAction(f"卸载 {array_path} ({current_mount_point})")
|
||||
unmount_action.triggered.connect(lambda: self._unmount_and_refresh(array_path))
|
||||
else:
|
||||
mount_action = menu.addAction(f"挂载 {array_path}...")
|
||||
mount_action.triggered.connect(lambda: self._handle_mount(array_path))
|
||||
create_raid_action = menu.addAction("创建 RAID 阵列...")
|
||||
create_raid_action.triggered.connect(self._handle_create_raid_array)
|
||||
menu.addSeparator()
|
||||
|
||||
stop_action = menu.addAction(f"停止阵列 {array_path}")
|
||||
stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path))
|
||||
if item and item.parent() is None: # 只针对顶层阵列项
|
||||
array_data = item.data(0, Qt.UserRole)
|
||||
if not array_data:
|
||||
logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。")
|
||||
return
|
||||
|
||||
delete_action = menu.addAction(f"删除阵列 {array_path}")
|
||||
delete_action.triggered.connect(lambda: self._handle_delete_raid_array(array_path, member_devices))
|
||||
array_path = array_data.get('device')
|
||||
array_state = array_data.get('state', 'N/A')
|
||||
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
|
||||
array_uuid = array_data.get('uuid') # <--- 获取 UUID
|
||||
|
||||
format_action = menu.addAction(f"格式化阵列 {array_path}...")
|
||||
format_action.triggered.connect(lambda: self._handle_format_raid_array(array_path))
|
||||
if not array_path or array_path == 'N/A':
|
||||
logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。")
|
||||
return
|
||||
|
||||
if menu.actions():
|
||||
menu.exec(self.ui.treeWidget_raid.mapToGlobal(pos))
|
||||
else:
|
||||
logger.info("右键点击了空白区域或没有可用的RAID操作。")
|
||||
if array_state == 'Stopped (Configured)':
|
||||
activate_action = menu.addAction(f"激活阵列 {array_path}")
|
||||
activate_action.triggered.connect(lambda: self._handle_activate_raid_array(array_path))
|
||||
# <--- 新增:删除停止状态阵列的配置条目
|
||||
delete_config_action = menu.addAction(f"删除配置文件条目 (UUID: {array_uuid})")
|
||||
delete_config_action.triggered.connect(lambda: self._handle_delete_configured_raid_array(array_uuid))
|
||||
# 停止状态的阵列不显示卸载、停止、删除、格式化等操作
|
||||
else: # 活动或降级状态的阵列
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
|
||||
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
|
||||
unmount_action = menu.addAction(f"卸载 {array_path} ({current_mount_point})")
|
||||
unmount_action.triggered.connect(lambda: self._unmount_and_refresh(array_path))
|
||||
else:
|
||||
mount_action = menu.addAction(f"挂载 {array_path}...")
|
||||
mount_action.triggered.connect(lambda: self._handle_mount(array_path))
|
||||
menu.addSeparator()
|
||||
|
||||
stop_action = menu.addAction(f"停止阵列 {array_path}")
|
||||
stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path))
|
||||
|
||||
# <--- 修改:删除活动阵列的动作,传递 UUID
|
||||
delete_action = menu.addAction(f"删除阵列 {array_path}")
|
||||
delete_action.triggered.connect(lambda: self._handle_delete_active_raid_array(array_path, member_devices, array_uuid))
|
||||
|
||||
format_action = menu.addAction(f"格式化阵列 {array_path}...")
|
||||
format_action.triggered.connect(lambda: self._handle_format_raid_array(array_path))
|
||||
|
||||
if menu.actions():
|
||||
menu.exec(self.ui.treeWidget_raid.mapToGlobal(pos))
|
||||
else:
|
||||
logger.info("右键点击了空白区域或没有可用的RAID操作。")
|
||||
|
||||
def _handle_activate_raid_array(self, array_path):
|
||||
"""
|
||||
处理激活已停止的 RAID 阵列。
|
||||
"""
|
||||
item = self.ui.treeWidget_raid.currentItem() # 获取当前选中的项
|
||||
if not item or item.parent() is not None: # 确保是顶层阵列项
|
||||
logger.warning("未选择有效的 RAID 阵列进行激活。")
|
||||
return
|
||||
|
||||
array_data = item.data(0, Qt.UserRole)
|
||||
if not array_data or array_data.get('device') != array_path:
|
||||
logger.error(f"获取 RAID 阵列 {array_path} 的数据失败或数据不匹配。")
|
||||
return
|
||||
|
||||
array_uuid = array_data.get('uuid')
|
||||
if not array_uuid or array_uuid == 'N/A':
|
||||
QMessageBox.critical(self, "错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
|
||||
logger.error(f"激活 RAID 阵列 {array_path} 失败:缺少 UUID。")
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认激活 RAID 阵列",
|
||||
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
|
||||
"这将尝试重新组装阵列。",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
|
||||
return
|
||||
|
||||
if self.raid_ops.activate_raid_array(array_path, array_uuid): # 传递 UUID
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_create_raid_array(self):
|
||||
available_devices = self.system_manager.get_unallocated_partitions()
|
||||
@@ -441,8 +492,18 @@ class MainWindow(QMainWindow):
|
||||
if self.raid_ops.stop_raid_array(array_path):
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_delete_raid_array(self, array_path, member_devices):
|
||||
if self.raid_ops.delete_raid_array(array_path, member_devices):
|
||||
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
|
||||
"""
|
||||
处理删除活动 RAID 阵列的操作。
|
||||
"""
|
||||
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid): # <--- 调用修改后的方法
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_delete_configured_raid_array(self, uuid): # <--- 新增方法
|
||||
"""
|
||||
处理删除停止状态 RAID 阵列的配置文件条目。
|
||||
"""
|
||||
if self.raid_ops.delete_configured_raid_array(uuid):
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_format_raid_array(self, array_path):
|
||||
@@ -789,3 +850,4 @@ if __name__ == "__main__":
|
||||
widget = MainWindow()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -343,29 +343,29 @@ class RaidOperations:
|
||||
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
|
||||
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
|
||||
if success_scan:
|
||||
# --- 新增:确保 /etc/mdadm 目录存在 ---
|
||||
mkdir_cmd = ["mkdir", "-p", "/etc/mdadm"]
|
||||
# --- 新增:确保 /etc 目录存在 ---
|
||||
mkdir_cmd = ["mkdir", "-p", "/etc"]
|
||||
success_mkdir, _, stderr_mkdir = self._execute_shell_command(
|
||||
mkdir_cmd,
|
||||
"创建 /etc/mdadm 目录失败"
|
||||
"创建 /etc 目录失败"
|
||||
)
|
||||
if not success_mkdir:
|
||||
logger.error(f"无法创建 /etc/mdadm 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
|
||||
QMessageBox.critical(None, "错误", f"无法创建 /etc/mdadm 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
||||
logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
|
||||
QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
||||
return False # 如果连目录都无法创建,则认为创建阵列失败,因为无法保证重启后自动识别
|
||||
logger.info("已确保 /etc/mdadm 目录存在。")
|
||||
logger.info("已确保 /etc 目录存在。")
|
||||
# --- 新增结束 ---
|
||||
|
||||
# 这里需要确保 /etc/mdadm/mdadm.conf 存在且可写入
|
||||
# 这里需要确保 /etc/mdadm.conf 存在且可写入
|
||||
# _execute_shell_command 会自动添加 sudo
|
||||
success_append, _, _ = self._execute_shell_command(
|
||||
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm/mdadm.conf > /dev/null"],
|
||||
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
|
||||
"更新 /etc/mdadm/mdadm.conf 失败"
|
||||
)
|
||||
if not success_append:
|
||||
logger.warning("更新 /etc/mdadm/mdadm.conf 失败。")
|
||||
logger.warning("更新 /etc/mdadm.conf 失败。")
|
||||
else:
|
||||
logger.info("已成功更新 /etc/mdadm/mdadm.conf。")
|
||||
logger.info("已成功更新 /etc/mdadm.conf。")
|
||||
else:
|
||||
logger.warning("未能扫描到 mdadm 配置,跳过更新 mdadm.conf。")
|
||||
|
||||
@@ -402,22 +402,23 @@ class RaidOperations:
|
||||
QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
|
||||
def delete_raid_array(self, array_path, member_devices):
|
||||
def delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
|
||||
"""
|
||||
删除一个 RAID 阵列。
|
||||
此操作将停止阵列并清除成员设备上的超级块。
|
||||
删除一个活动的 RAID 阵列。
|
||||
此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。
|
||||
:param array_path: RAID 阵列的设备路径,例如 /dev/md0
|
||||
:param member_devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
|
||||
:param uuid: RAID 阵列的 UUID
|
||||
"""
|
||||
reply = QMessageBox.question(None, "确认删除 RAID 阵列",
|
||||
f"你确定要删除 RAID 阵列 {array_path} 吗?\n"
|
||||
"此操作将停止阵列并清除成员设备上的 RAID 超级块,数据将无法访问!",
|
||||
"此操作将停止阵列,清除成员设备上的 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}")
|
||||
logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})")
|
||||
|
||||
# 1. 停止阵列
|
||||
if not self.stop_raid_array(array_path):
|
||||
@@ -444,6 +445,15 @@ class RaidOperations:
|
||||
success_all_cleared = False
|
||||
logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。")
|
||||
|
||||
# 3. 从 mdadm.conf 中删除配置条目
|
||||
try:
|
||||
self.system_manager.delete_raid_array_config(uuid)
|
||||
logger.info(f"已成功从 mdadm.conf 中删除 UUID 为 {uuid} 的 RAID 阵列配置。")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||
logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||
return False # 如果配置文件删除失败,也视为整体操作失败
|
||||
|
||||
if success_all_cleared:
|
||||
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
|
||||
QMessageBox.information(None, "成功", f"成功删除 RAID 阵列 {array_path}。")
|
||||
@@ -451,3 +461,58 @@ class RaidOperations:
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
|
||||
return False
|
||||
|
||||
def delete_configured_raid_array(self, uuid): # <--- 新增方法
|
||||
"""
|
||||
只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。
|
||||
:param uuid: 要删除的 RAID 阵列的 UUID。
|
||||
:return: True 如果成功,False 如果失败。
|
||||
"""
|
||||
reply = QMessageBox.question(None, "确认删除 RAID 阵列配置",
|
||||
f"您确定要删除 UUID 为 {uuid} 的 RAID 阵列配置吗?\n"
|
||||
"此操作将从配置文件中移除该条目,但不会影响实际的磁盘数据或活动阵列。",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了删除 UUID 为 {uuid} 的 RAID 阵列配置的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除 UUID 为 {uuid} 的 RAID 阵列配置文件条目。")
|
||||
try:
|
||||
self.system_manager.delete_raid_array_config(uuid)
|
||||
QMessageBox.information(None, "成功", f"成功删除 UUID 为 {uuid} 的 RAID 阵列配置。")
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列配置失败: {e}")
|
||||
logger.error(f"删除 RAID 阵列配置失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def activate_raid_array(self, array_path, array_uuid): # 添加 array_uuid 参数
|
||||
"""
|
||||
激活一个已停止的 RAID 阵列。
|
||||
使用 mdadm --assemble <device> --uuid=<uuid>
|
||||
"""
|
||||
if not array_uuid or array_uuid == 'N/A':
|
||||
QMessageBox.critical(None, "错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
|
||||
logger.error(f"激活 RAID 阵列 {array_path} 失败:缺少 UUID。")
|
||||
return False
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"成功激活 RAID 阵列 {array_path} (UUID: {array_uuid})。")
|
||||
QMessageBox.information(None, "成功", f"成功激活 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"激活 RAID 阵列 {array_path} 失败。")
|
||||
return False
|
||||
|
||||
|
||||
364
system_info.py
364
system_info.py
@@ -73,8 +73,14 @@ class SystemInfoManager:
|
||||
add_path_recursive(dev['children'])
|
||||
add_path_recursive(devices)
|
||||
return devices
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"获取块设备信息失败: {e.stderr.strip()}")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析 lsblk JSON 输出失败: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"获取块设备信息失败: {e}")
|
||||
logger.error(f"获取块设备信息失败 (未知错误): {e}")
|
||||
return []
|
||||
|
||||
def _find_device_by_path_recursive(self, dev_list, target_path):
|
||||
@@ -199,40 +205,12 @@ class SystemInfoManager:
|
||||
logger.debug(f"get_mountpoint_for_device: 未能获取到 {original_device_path} 的挂载点。")
|
||||
return None
|
||||
|
||||
def get_mdadm_arrays(self):
|
||||
def _parse_mdadm_detail_output(self, output, device_path):
|
||||
"""
|
||||
获取 mdadm RAID 阵列信息。
|
||||
解析 mdadm --detail 命令的输出。
|
||||
"""
|
||||
cmd = ["mdadm", "--detail", "--scan"]
|
||||
try:
|
||||
stdout, _ = self._run_command(cmd)
|
||||
arrays = []
|
||||
for line in stdout.splitlines():
|
||||
if line.startswith("ARRAY"):
|
||||
parts = line.split(' ')
|
||||
array_path = parts[1] # e.g., /dev/md0 or /dev/md/new_raid
|
||||
# Use mdadm --detail for more specific info
|
||||
detail_cmd = ["mdadm", "--detail", array_path]
|
||||
detail_stdout, _ = self._run_command(detail_cmd)
|
||||
array_info = self._parse_mdadm_detail(detail_stdout, array_path)
|
||||
arrays.append(array_info)
|
||||
return arrays
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "No arrays found" in e.stderr or "No arrays found" in e.stdout: # mdadm --detail --scan might exit 1 if no arrays
|
||||
logger.info("未找到任何RAID阵列。")
|
||||
return []
|
||||
logger.error(f"获取RAID阵列信息失败: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"获取RAID阵列信息失败: {e}")
|
||||
return []
|
||||
|
||||
def _parse_mdadm_detail(self, detail_output, array_path):
|
||||
"""
|
||||
解析 mdadm --detail 的输出。
|
||||
"""
|
||||
info = {
|
||||
'device': array_path,
|
||||
array_info = {
|
||||
'device': device_path, # Default to input path, will be updated by canonical_device_path
|
||||
'level': 'N/A',
|
||||
'state': 'N/A',
|
||||
'array_size': 'N/A',
|
||||
@@ -241,46 +219,202 @@ class SystemInfoManager:
|
||||
'spare_devices': 'N/A',
|
||||
'total_devices': 'N/A',
|
||||
'uuid': 'N/A',
|
||||
'name': 'N/A',
|
||||
'name': os.path.basename(device_path), # Default name, will be overridden
|
||||
'chunk_size': 'N/A',
|
||||
'member_devices': []
|
||||
}
|
||||
member_pattern = re.compile(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+?)\s+(/dev/.+)$')
|
||||
|
||||
for line in detail_output.splitlines():
|
||||
if "Raid Level :" in line:
|
||||
info['level'] = line.split(':')[-1].strip()
|
||||
elif "Array Size :" in line:
|
||||
info['array_size'] = line.split(':')[-1].strip()
|
||||
elif "State :" in line:
|
||||
info['state'] = line.split(':')[-1].strip()
|
||||
elif "Active Devices :" in line:
|
||||
info['active_devices'] = line.split(':')[-1].strip()
|
||||
elif "Failed Devices :" in line:
|
||||
info['failed_devices'] = line.split(':')[-1].strip()
|
||||
elif "Spare Devices :" in line:
|
||||
info['spare_devices'] = line.split(':')[-1].strip()
|
||||
elif "Total Devices :" in line:
|
||||
info['total_devices'] = line.split(':')[-1].strip()
|
||||
elif "UUID :" in line:
|
||||
info['uuid'] = line.split(':')[-1].strip()
|
||||
elif "Name :" in line:
|
||||
info['name'] = line.split(':')[-1].strip()
|
||||
elif "Chunk Size :" in line:
|
||||
info['chunk_size'] = line.split(':')[-1].strip()
|
||||
# 首先,尝试从 mdadm --detail 输出的第一行获取规范的设备路径
|
||||
device_path_header_match = re.match(r'^(?P<canonical_path>/dev/md\d+):', output)
|
||||
if device_path_header_match:
|
||||
array_info['device'] = device_path_header_match.group('canonical_path')
|
||||
array_info['name'] = os.path.basename(array_info['device']) # 根据规范路径更新默认名称
|
||||
|
||||
# Member devices
|
||||
match = member_pattern.match(line)
|
||||
if match:
|
||||
member_info = {
|
||||
'number': match.group(1),
|
||||
'major': match.group(2),
|
||||
'minor': match.group(3),
|
||||
'raid_device': match.group(4),
|
||||
'device_path': match.group(5)
|
||||
}
|
||||
info['member_devices'].append(member_info)
|
||||
return info
|
||||
# 逐行解析键值对
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# 尝试匹配 "Key : Value" 格式
|
||||
kv_match = re.match(r'^(?P<key>[A-Za-z ]+)\s*:\s*(?P<value>.+)', line)
|
||||
if kv_match:
|
||||
key = kv_match.group('key').strip().lower().replace(' ', '_')
|
||||
value = kv_match.group('value').strip()
|
||||
|
||||
if key == 'raid_level':
|
||||
array_info['level'] = value
|
||||
elif key == 'state':
|
||||
array_info['state'] = value
|
||||
elif key == 'array_size':
|
||||
array_info['array_size'] = value
|
||||
elif key == 'uuid':
|
||||
array_info['uuid'] = value
|
||||
elif key == 'raid_devices':
|
||||
array_info['total_devices'] = value
|
||||
elif key == 'active_devices':
|
||||
array_info['active_devices'] = value
|
||||
elif key == 'failed_devices':
|
||||
array_info['failed_devices'] = value
|
||||
elif key == 'spare_devices':
|
||||
array_info['spare_devices'] = value
|
||||
elif key == 'name':
|
||||
# 只取名字的第一部分,忽略括号内的额外信息
|
||||
array_info['name'] = value.split(' ')[0]
|
||||
elif key == 'chunk_size':
|
||||
array_info['chunk_size'] = value
|
||||
|
||||
# 成员设备解析 (独立于键值对,因为它有固定格式)
|
||||
member_pattern = re.match(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([^\s]+(?:\s+[^\s]+)*)\s+(/dev/\S+)$', line)
|
||||
if member_pattern:
|
||||
array_info['member_devices'].append({
|
||||
'number': member_pattern.group(1),
|
||||
'major': member_pattern.group(2),
|
||||
'minor': member_pattern.group(3),
|
||||
'raid_device': member_pattern.group(4),
|
||||
'state': member_pattern.group(5).strip(),
|
||||
'device_path': member_pattern.group(6)
|
||||
})
|
||||
|
||||
# 最终状态标准化
|
||||
if array_info['state'] and 'active' in array_info['state'].lower():
|
||||
array_info['state'] = array_info['state'].replace('active', 'Active')
|
||||
elif array_info['state'] == 'clean':
|
||||
array_info['state'] = 'Active, Clean'
|
||||
elif array_info['state'] == 'N/A': # 如果未明确找到状态,则回退到推断
|
||||
if "clean" in output.lower() and "active" in output.lower():
|
||||
array_info['state'] = "Active, Clean"
|
||||
elif "degraded" in output.lower():
|
||||
array_info['state'] = "Degraded"
|
||||
elif "inactive" in output.lower() or "stopped" in output.lower():
|
||||
array_info['state'] = "Stopped"
|
||||
|
||||
return array_info
|
||||
|
||||
def get_mdadm_arrays(self):
|
||||
"""
|
||||
获取所有 RAID 阵列的信息,包括活动的和已停止的。
|
||||
"""
|
||||
all_arrays_info = {} # 使用 UUID 作为键,存储阵列的详细信息
|
||||
|
||||
# 1. 获取活动的 RAID 阵列信息 (通过 mdadm --detail --scan 和 /proc/mdstat)
|
||||
active_md_devices = []
|
||||
|
||||
# 从 mdadm --detail --scan 获取
|
||||
try:
|
||||
stdout_scan, stderr_scan = self._run_command(["mdadm", "--detail", "--scan"], check_output=True)
|
||||
for line in stdout_scan.splitlines():
|
||||
if line.startswith("ARRAY"):
|
||||
match = re.match(r'ARRAY\s+(\S+)(?:\s+\S+=\S+)*\s+UUID=([0-9a-f:]+)', line)
|
||||
if match:
|
||||
dev_path = match.group(1)
|
||||
if dev_path not in active_md_devices:
|
||||
active_md_devices.append(dev_path)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "No arrays found" not in e.stderr and "No arrays found" not in e.stdout:
|
||||
logger.warning(f"执行 mdadm --detail --scan 失败: {e.stderr.strip()}")
|
||||
except Exception as e:
|
||||
logger.warning(f"处理 mdadm --detail --scan 输出时发生错误: {e}")
|
||||
|
||||
|
||||
# 从 /proc/mdstat 获取 (补充可能未被 --scan 报告的活动阵列)
|
||||
try:
|
||||
with open("/proc/mdstat", "r") as f:
|
||||
mdstat_content = f.read()
|
||||
for line in mdstat_content.splitlines():
|
||||
if line.startswith("md") and "active" in line:
|
||||
device_name_match = re.match(r'^(md\d+)\s+:', line)
|
||||
if device_name_match:
|
||||
dev_path = f"/dev/{device_name_match.group(1)}"
|
||||
if dev_path not in active_md_devices:
|
||||
active_md_devices.append(dev_path)
|
||||
except FileNotFoundError:
|
||||
logger.debug("/proc/mdstat not found. Cannot check active arrays from /proc/mdstat.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading /proc/mdstat: {e}")
|
||||
|
||||
# 现在,对每个找到的活动 MD 设备获取其详细信息
|
||||
for device_path in active_md_devices:
|
||||
try:
|
||||
# 解析符号链接以获取规范路径,例如 /dev/md/0 -> /dev/md0
|
||||
canonical_device_path = self._get_actual_md_device_path(device_path)
|
||||
if not canonical_device_path:
|
||||
canonical_device_path = device_path # 如果解析失败,使用原始路径作为回退
|
||||
|
||||
detail_stdout, detail_stderr = self._run_command(["mdadm", "--detail", canonical_device_path], check_output=True)
|
||||
array_info = self._parse_mdadm_detail_output(detail_stdout, canonical_device_path)
|
||||
if array_info and array_info.get('uuid') and array_info.get('device'):
|
||||
# _parse_mdadm_detail_output 现在会更新 array_info['device'] 为规范路径
|
||||
# 使用 UUID 作为键,如果同一个阵列有多个表示,只保留一个(通常是活动的)
|
||||
all_arrays_info[array_info['uuid']] = array_info
|
||||
else:
|
||||
logger.warning(f"无法从 mdadm --detail {canonical_device_path} 输出中解析 RAID 阵列信息: {detail_stdout[:100]}...")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"获取 RAID 阵列 {device_path} 详细信息失败: {e.stderr.strip()}")
|
||||
except Exception as e:
|
||||
logger.warning(f"处理 RAID 阵列 {device_path} 详细信息时发生错误: {e}")
|
||||
|
||||
|
||||
# 2. 从 /etc/mdadm.conf 获取配置的 RAID 阵列信息 (可能包含已停止的)
|
||||
mdadm_conf_paths = ["/etc/mdadm/mdadm.conf", "/etc/mdadm.conf"]
|
||||
found_conf = False
|
||||
for mdadm_conf_path in mdadm_conf_paths:
|
||||
if os.path.exists(mdadm_conf_path):
|
||||
found_conf = True
|
||||
try:
|
||||
with open(mdadm_conf_path, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("ARRAY"):
|
||||
match_base = re.match(r'ARRAY\s+(\S+)\s*(.*)', line)
|
||||
if match_base:
|
||||
device_path = match_base.group(1)
|
||||
rest_of_line = match_base.group(2)
|
||||
|
||||
uuid = 'N/A'
|
||||
name = os.path.basename(device_path) # Default name
|
||||
|
||||
uuid_match_conf = re.search(r'UUID=([0-9a-f:]+)', rest_of_line)
|
||||
if uuid_match_conf:
|
||||
uuid = uuid_match_conf.group(1)
|
||||
|
||||
name_match_conf = re.search(r'NAME=(\S+)', rest_of_line)
|
||||
if name_match_conf:
|
||||
name = name_match_conf.group(1)
|
||||
|
||||
if uuid != 'N/A': # 只有成功提取到 UUID 才添加
|
||||
# 只有当此 UUID 对应的阵列尚未被识别为活动状态时,才添加为停止状态
|
||||
if uuid not in all_arrays_info:
|
||||
all_arrays_info[uuid] = {
|
||||
'device': device_path,
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'level': 'Unknown',
|
||||
'state': 'Stopped (Configured)',
|
||||
'array_size': 'N/A',
|
||||
'active_devices': 'N/A',
|
||||
'failed_devices': 'N/A',
|
||||
'spare_devices': 'N/A',
|
||||
'total_devices': 'N/A',
|
||||
'chunk_size': 'N/A',
|
||||
'member_devices': []
|
||||
}
|
||||
else:
|
||||
logger.warning(f"无法从 mdadm.conf 行 '{line}' 中提取 UUID。")
|
||||
else:
|
||||
logger.warning(f"无法解析 mdadm.conf 中的 ARRAY 行: '{line}'")
|
||||
except Exception as e:
|
||||
logger.warning(f"读取或解析 {mdadm_conf_path} 失败: {e}")
|
||||
break # 找到并处理了一个配置文件就退出循环
|
||||
|
||||
if not found_conf:
|
||||
logger.info(f"未找到 mdadm 配置文件。")
|
||||
|
||||
|
||||
# 返回所有阵列的列表
|
||||
# 排序:活动阵列在前,然后是停止的
|
||||
sorted_arrays = sorted(all_arrays_info.values(), key=lambda x: (x.get('state') != 'Active', x.get('device')))
|
||||
return sorted_arrays
|
||||
|
||||
def get_lvm_info(self):
|
||||
"""
|
||||
@@ -290,7 +424,7 @@ class SystemInfoManager:
|
||||
|
||||
# Get PVs
|
||||
try:
|
||||
stdout, _ = self._run_command(["pvs", "--reportformat", "json"])
|
||||
stdout, stderr = self._run_command(["pvs", "--reportformat", "json"])
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
for pv_data in data['report'][0].get('pv', []):
|
||||
@@ -307,13 +441,15 @@ class SystemInfoManager:
|
||||
if "No physical volume found" in e.stderr or "No physical volumes found" in e.stdout:
|
||||
logger.info("未找到任何LVM物理卷。")
|
||||
else:
|
||||
logger.error(f"获取LVM物理卷信息失败: {e}")
|
||||
logger.error(f"获取LVM物理卷信息失败: {e.stderr.strip()}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析LVM物理卷JSON输出失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取LVM物理卷信息失败: {e}")
|
||||
logger.error(f"获取LVM物理卷信息失败 (未知错误): {e}")
|
||||
|
||||
# Get VGs
|
||||
try:
|
||||
stdout, _ = self._run_command(["vgs", "--reportformat", "json"])
|
||||
stdout, stderr = self._run_command(["vgs", "--reportformat", "json"])
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
for vg_data in data['report'][0].get('vg', []):
|
||||
@@ -332,14 +468,16 @@ class SystemInfoManager:
|
||||
if "No volume group found" in e.stderr or "No volume groups found" in e.stdout:
|
||||
logger.info("未找到任何LVM卷组。")
|
||||
else:
|
||||
logger.error(f"获取LVM卷组信息失败: {e}")
|
||||
logger.error(f"获取LVM卷组信息失败: {e.stderr.strip()}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析LVM卷组JSON输出失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取LVM卷组信息失败: {e}")
|
||||
logger.error(f"获取LVM卷组信息失败 (未知错误): {e}")
|
||||
|
||||
# Get LVs (MODIFIED: added -o lv_path)
|
||||
try:
|
||||
# 明确请求 lv_path,因为默认的 --reportformat json 不包含它
|
||||
stdout, _ = self._run_command(["lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", "--reportformat", "json"])
|
||||
stdout, stderr = self._run_command(["lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", "--reportformat", "json"])
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
for lv_data in data['report'][0].get('lv', []):
|
||||
@@ -357,9 +495,11 @@ class SystemInfoManager:
|
||||
if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout:
|
||||
logger.info("未找到任何LVM逻辑卷。")
|
||||
else:
|
||||
logger.error(f"获取LVM逻辑卷信息失败: {e}")
|
||||
logger.error(f"获取LVM逻辑卷信息失败: {e.stderr.strip()}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析LVM逻辑卷JSON输出失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取LVM逻辑卷信息失败: {e}")
|
||||
logger.error(f"获取LVM逻辑卷信息失败 (未知错误): {e}")
|
||||
|
||||
return lvm_info
|
||||
|
||||
@@ -430,6 +570,10 @@ class SystemInfoManager:
|
||||
logger.warning(f"传入 _get_actual_md_device_path 的 array_path 不是字符串: {array_path} (类型: {type(array_path)})")
|
||||
return None
|
||||
|
||||
# 如果已经是规范的 /dev/mdX 路径,直接返回
|
||||
if re.match(r'^/dev/md\d+$', array_path):
|
||||
return array_path
|
||||
|
||||
if os.path.exists(array_path):
|
||||
try:
|
||||
# os.path.realpath 会解析符号链接,例如 /dev/md/new_raid -> /dev/md127
|
||||
@@ -546,3 +690,69 @@ class SystemInfoManager:
|
||||
# 5. 如果仍然没有找到,返回 None
|
||||
logger.debug(f"get_device_details_by_path: 未能获取到 {original_device_path} 的任何详情。")
|
||||
return None
|
||||
|
||||
def delete_raid_array_config(self, uuid):
|
||||
"""
|
||||
从 mdadm.conf 文件中删除指定 UUID 的 RAID 阵列配置条目。
|
||||
:param uuid: 要删除的 RAID 阵列的 UUID。
|
||||
:return: True 如果成功删除或未找到条目,False 如果发生错误。
|
||||
:raises Exception: 如果删除失败。
|
||||
"""
|
||||
logger.info(f"尝试从 mdadm.conf 中删除 UUID 为 {uuid} 的 RAID 阵列配置。")
|
||||
mdadm_conf_paths = ["/etc/mdadm/mdadm.conf", "/etc/mdadm.conf"]
|
||||
target_conf_path = None
|
||||
|
||||
# 找到存在的 mdadm.conf 文件
|
||||
for path in mdadm_conf_paths:
|
||||
if os.path.exists(path):
|
||||
target_conf_path = path
|
||||
break
|
||||
|
||||
if not target_conf_path:
|
||||
logger.warning("未找到 mdadm 配置文件,无法删除配置条目。")
|
||||
raise FileNotFoundError("mdadm 配置文件未找到。")
|
||||
|
||||
original_lines = []
|
||||
try:
|
||||
with open(target_conf_path, 'r') as f:
|
||||
original_lines = f.readlines()
|
||||
except Exception as e:
|
||||
logger.error(f"读取 mdadm 配置文件 {target_conf_path} 失败: {e}")
|
||||
raise Exception(f"读取 mdadm 配置文件失败: {e}")
|
||||
|
||||
new_lines = []
|
||||
entry_found = False
|
||||
for line in original_lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line.startswith("ARRAY"):
|
||||
# 尝试匹配 UUID
|
||||
uuid_match = re.search(r'UUID=([0-9a-f:]+)', stripped_line)
|
||||
if uuid_match and uuid_match.group(1) == uuid:
|
||||
logger.debug(f"找到并跳过 UUID 为 {uuid} 的配置行: {stripped_line}")
|
||||
entry_found = True
|
||||
continue # 跳过此行,即删除
|
||||
new_lines.append(line)
|
||||
|
||||
if not entry_found:
|
||||
logger.warning(f"在 mdadm.conf 中未找到 UUID 为 {uuid} 的 RAID 阵列配置条目。")
|
||||
# 即使未找到,也不视为错误,因为可能已经被手动删除或从未写入
|
||||
return True
|
||||
|
||||
# 将修改后的内容写回文件 (需要 root 权限)
|
||||
try:
|
||||
# 使用临时文件进行原子写入,防止数据损坏
|
||||
temp_file_path = f"{target_conf_path}.tmp"
|
||||
with open(temp_file_path, 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
# 替换原文件
|
||||
self._run_command(["mv", temp_file_path, target_conf_path], root_privilege=True)
|
||||
logger.info(f"成功从 {target_conf_path} 中删除 UUID 为 {uuid} 的 RAID 阵列配置。")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"删除 mdadm 配置条目失败 (mv 命令错误): {e.stderr.strip()}")
|
||||
raise Exception(f"删除 mdadm 配置条目失败: {e.stderr.strip()}")
|
||||
except Exception as e:
|
||||
logger.error(f"写入 mdadm 配置文件 {target_conf_path} 失败: {e}")
|
||||
raise Exception(f"写入 mdadm 配置文件失败: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user