From dd51a102398c79b0180a95a2aecc65761c2272d1 Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.comm> Date: Tue, 3 Feb 2026 15:18:43 +0800 Subject: [PATCH] fix 44 --- __pycache__/raid_operations.cpython-314.pyc | Bin 24012 -> 27706 bytes __pycache__/system_info.cpython-314.pyc | Bin 31357 -> 42275 bytes mainwindow.py | 164 ++++++--- raid_operations.py | 95 ++++- system_info.py | 364 +++++++++++++++----- 5 files changed, 480 insertions(+), 143 deletions(-) diff --git a/__pycache__/raid_operations.cpython-314.pyc b/__pycache__/raid_operations.cpython-314.pyc index 3e948abf72d0ac41003d4a835f563e0a79aafa55..f6d2e0b7162fc10da4a5fdb5143fc669c2c0d388 100644 GIT binary patch delta 4142 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 delta 1162 zcmaJV=Cd zQ&W4nsONu$%_h52>+3IADwDGpGr)s#$H!$O@4H+kixf}a)B3rS*+J9v26TP3eW)W)TDS`*7Zsp)9 zK{@vHQ0oX4^-@0@D;JsK4?O2QDbv1rW`9UF4JtAwbmw)c%C?_ace1bJdm2ZRrWY-gktU0IXlmk+JF|9zh7RE}O^Y;Sg)=aH z_(=%0YT?tK#ukmB6)}SZT@$@~GuDa(wG9bOC{LtV)OHr3kV?=C2BAC87b;I!AIS-Ej` zdFHdscmf9cMxfYx081cxZJYc~nDw?*my-;cYvU_ZWF# zM|VKIzZ5!OnuE7rzJPVW9<}bCB!ui#pjm+~1x5%sbuy#)Ul6!!7?y@#k>r7ZbTa6d z!Z32QwJ)qAjKksJHci$m%A$aDT#85ovM(4I@X508)Ll*>epg76eUad6C&G1)DMrbA zg>Mmvp>+dr-Q{ZKALD8sT!B6Ce!m$$>}FuL{}*b*4s*w(VwokSoLE*ma8jL}PCaxF ycID?rNy4>3FKz>Hr diff --git a/__pycache__/system_info.cpython-314.pyc b/__pycache__/system_info.cpython-314.pyc index fcb119b3dc2f2983bb8bebf5e489f82c8faa4080..0f16a3c02ce18bc038149b8c72fe54d98f55aa49 100644 GIT binary patch delta 14842 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 SNB7^f$Xa5c>RUjNWBnglM#i%M diff --git a/mainwindow.py b/mainwindow.py index f3eed01..165d452 100644 --- a/mainwindow.py +++ b/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()) + diff --git a/raid_operations.py b/raid_operations.py index 751db5c..73c69ff 100644 --- a/raid_operations.py +++ b/raid_operations.py @@ -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 --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 --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 + diff --git a/system_info.py b/system_info.py index b264e40..275a604 100644 --- a/system_info.py +++ b/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/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[A-Za-z ]+)\s*:\s*(?P.+)', 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}") +