fix bug45
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE QtCreatorProject>
|
||||
<!-- Written by QtCreator 18.0.2, 2026-02-02T03:36:12. -->
|
||||
<!-- Written by QtCreator 18.0.2, 2026-02-02T21:58:57. -->
|
||||
<qtcreator>
|
||||
<data>
|
||||
<variable>EnvironmentId</variable>
|
||||
@@ -99,13 +99,15 @@
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{2b4075d3-005b-46f6-8fd5-3bd8a94071db}</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">Python.PysideBuildStep</value>
|
||||
<value type="QString" key="Python.PySideProjectTool">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-project</value>
|
||||
<value type="QString" key="Python.PySideUic">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-uic</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">构建</value>
|
||||
@@ -126,7 +128,7 @@
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Python 3.14.2 Virtual Environment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">Python.PySideBuildConfiguration</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||
@@ -224,7 +226,70 @@
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">4</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.4">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">dialogs.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/dialogs.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/dialogs.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.5">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">raid_operations.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/raid_operations.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/raid_operations.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.6">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">lvm_operations.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">7</value>
|
||||
<value type="QString" key="python">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/python</value>
|
||||
<value type="QString" key="venv">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2</value>
|
||||
</valuemap>
|
||||
@@ -326,7 +391,70 @@
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">4</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.4">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">dialogs.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/dialogs.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/dialogs.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.5">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">raid_operations.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/raid_operations.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/raid_operations.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.6">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">lvm_operations.py</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
|
||||
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
|
||||
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">7</value>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
dialogs.py
28
dialogs.py
@@ -388,36 +388,46 @@ class CreateLvDialog(QDialog):
|
||||
max_size_gb = self.vg_sizes.get(selected_vg, 0.0)
|
||||
|
||||
# 确保 max_size_gb 至少是 spinbox 的最小值,以防卷组可用空间过小导致 UI 问题
|
||||
if max_size_gb < self.lv_size_spinbox.minimum():
|
||||
# 如果实际最大可用空间小于最小允许值,则将最小值临时调整为实际最大值
|
||||
current_min_limit = self.lv_size_spinbox.minimum() # 获取当前的最小值限制
|
||||
if max_size_gb < current_min_limit:
|
||||
# 如果实际最大可用空间小于当前最小值限制,则将最小值临时调整为实际最大值
|
||||
self.lv_size_spinbox.setMinimum(max_size_gb if max_size_gb > 0 else 0.01) # 至少0.01GB
|
||||
else:
|
||||
self.lv_size_spinbox.setMinimum(0.1) # 恢复正常最小值
|
||||
|
||||
self.lv_size_spinbox.setMaximum(max(self.lv_size_spinbox.minimum(), max_size_gb)) # 设置最大值,确保不小于最小值
|
||||
# 计算并设置 spinbox 的最大值
|
||||
clamped_max_gb = max(self.lv_size_spinbox.minimum(), max_size_gb)
|
||||
self.lv_size_spinbox.setMaximum(clamped_max_gb)
|
||||
|
||||
# 如果选中了“使用最大可用空间”,则将 spinbox 值设置为最大值
|
||||
if self.use_max_space_checkbox.isChecked():
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum())
|
||||
self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值
|
||||
else:
|
||||
# 如果当前值超过了新的最大值,则调整为新的最大值
|
||||
if self.lv_size_spinbox.value() > self.lv_size_spinbox.maximum():
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum())
|
||||
if self.lv_size_spinbox.value() > clamped_max_gb:
|
||||
self.lv_size_spinbox.setValue(clamped_max_gb)
|
||||
# 如果当前值小于新的最小值,则调整
|
||||
elif self.lv_size_spinbox.value() < self.lv_size_spinbox.minimum():
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum())
|
||||
|
||||
|
||||
def _toggle_size_input(self, state):
|
||||
"""根据“使用最大可用空间”复选框的状态切换大小输入框的启用/禁用状态。"""
|
||||
# 获取当前选中的卷组的实际最大可用空间(GB)
|
||||
selected_vg = self.vg_combo_box.currentText()
|
||||
actual_max_gb = self.vg_sizes.get(selected_vg, 0.0)
|
||||
|
||||
# 确保计算出的最大值不小于spinbox的最小值
|
||||
clamped_max_gb = max(self.lv_size_spinbox.minimum(), actual_max_gb)
|
||||
|
||||
if state == Qt.Checked:
|
||||
self.lv_size_spinbox.setDisabled(True)
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) # 设置为最大值
|
||||
self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值
|
||||
else:
|
||||
self.lv_size_spinbox.setDisabled(False)
|
||||
# 如果之前是最大值,取消勾选后,恢复到最小值或一个合理值
|
||||
if self.lv_size_spinbox.value() == self.lv_size_spinbox.maximum():
|
||||
if self.lv_size_spinbox.value() == clamped_max_gb: # 比较时也使用计算出的最大值
|
||||
self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum())
|
||||
# 否则,保持当前值(用户手动输入的值)
|
||||
|
||||
|
||||
def get_lv_info(self):
|
||||
|
||||
@@ -3,12 +3,13 @@ import logging
|
||||
import re
|
||||
import os
|
||||
from PySide6.QtWidgets import QMessageBox, QInputDialog
|
||||
from system_info import SystemInfoManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DiskOperations:
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, system_manager: SystemInfoManager): # NEW: 接收 system_manager 实例
|
||||
self.system_manager = system_manager
|
||||
|
||||
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=None, input_data=None):
|
||||
@@ -18,6 +19,7 @@ class DiskOperations:
|
||||
:param error_message: 命令失败时显示给用户的错误消息。
|
||||
:param root_privilege: 如果为 True,则使用 sudo 执行命令。
|
||||
:param suppress_critical_dialog_on_stderr_match: 如果 stderr 包含此字符串,则不显示关键错误对话框。
|
||||
可以是字符串或字符串元组。
|
||||
:param input_data: 传递给命令stdin的数据 (str)。
|
||||
:return: (True/False, stdout_str, stderr_str)
|
||||
"""
|
||||
@@ -50,10 +52,17 @@ class DiskOperations:
|
||||
logger.error(f"标准输出: {e.stdout.strip()}")
|
||||
logger.error(f"标准错误: {stderr_output}")
|
||||
|
||||
if suppress_critical_dialog_on_stderr_match and \
|
||||
(suppress_critical_dialog_on_stderr_match in stderr_output or \
|
||||
(isinstance(suppress_critical_dialog_on_stderr_match, tuple) and \
|
||||
any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match))):
|
||||
# Determine if the error dialog should be suppressed by this function
|
||||
should_suppress_dialog_here = 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_here = True
|
||||
elif isinstance(suppress_critical_dialog_on_stderr_match, tuple):
|
||||
if any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match):
|
||||
should_suppress_dialog_here = True
|
||||
|
||||
if should_suppress_dialog_here:
|
||||
logger.info(f"错误信息 '{stderr_output}' 匹配抑制条件,不显示关键错误对话框。")
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}")
|
||||
@@ -81,7 +90,9 @@ class DiskOperations:
|
||||
fstab_path = "/etc/fstab"
|
||||
|
||||
try:
|
||||
# 检查 fstab 中是否已存在相同 UUID 或挂载点的条目
|
||||
# 检查 fstab 中是否已存在相同 UUID 的条目
|
||||
# 使用 _execute_shell_command 来读取 fstab,尽管通常不需要 sudo
|
||||
# 这里为了简化,直接读取文件,但写入时使用 _execute_shell_command
|
||||
with open(fstab_path, 'r') as f:
|
||||
fstab_content = f.readlines()
|
||||
|
||||
@@ -90,21 +101,23 @@ class DiskOperations:
|
||||
logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。")
|
||||
QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。")
|
||||
return True # 认为成功,因为目标已达成
|
||||
# 检查挂载点是否已存在 (可能被其他设备使用)
|
||||
if mount_point in line.split():
|
||||
logger.warning(f"挂载点 {mount_point} 已存在于 fstab 中。跳过添加。")
|
||||
QMessageBox.information(None, "信息", f"挂载点 {mount_point} 已存在于 fstab 中。")
|
||||
return True # 认为成功,因为目标已达成
|
||||
|
||||
# 如果不存在,则追加到 fstab
|
||||
with open(fstab_path, 'a') as f:
|
||||
f.write(fstab_entry + '\n')
|
||||
# 使用 _execute_shell_command for sudo write
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"],
|
||||
f"将 {device_path} 添加到 {fstab_path} 失败",
|
||||
root_privilege=True
|
||||
)
|
||||
if success:
|
||||
logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。")
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。")
|
||||
return True
|
||||
else:
|
||||
return False # Error handled by _execute_shell_command
|
||||
except Exception as e:
|
||||
logger.error(f"写入 {fstab_path} 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"写入 {fstab_path} 失败: {e}")
|
||||
logger.error(f"处理 {fstab_path} 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"处理 {fstab_path} 失败: {e}")
|
||||
return False
|
||||
|
||||
def _remove_fstab_entry(self, device_path):
|
||||
@@ -112,49 +125,45 @@ class DiskOperations:
|
||||
从 /etc/fstab 中移除指定设备的条目。
|
||||
此方法需要 SystemInfoManager 来获取设备的 UUID。
|
||||
"""
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["lsblk", "-no", "UUID", device_path],
|
||||
f"获取设备 {device_path} 的 UUID 失败",
|
||||
root_privilege=False, # lsblk 通常不需要 sudo
|
||||
suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") # 匹配中英文错误
|
||||
)
|
||||
if not success:
|
||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。错误: {stderr}")
|
||||
return False
|
||||
|
||||
uuid = stdout.strip()
|
||||
if not uuid:
|
||||
logger.warning(f"设备 {device_path} 没有 UUID,无法从 fstab 中移除。")
|
||||
# NEW: 使用 system_manager 获取 UUID,它会处理路径解析
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
if not device_details or not device_details.get('uuid'):
|
||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。")
|
||||
return False
|
||||
uuid = device_details.get('uuid')
|
||||
|
||||
fstab_path = "/etc/fstab"
|
||||
try:
|
||||
with open(fstab_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
removed = False
|
||||
for line in lines:
|
||||
if f"UUID={uuid}" in line:
|
||||
logger.info(f"从 {fstab_path} 中移除了条目: {line.strip()}")
|
||||
removed = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if removed:
|
||||
# 写入临时文件,然后替换原文件
|
||||
with open(fstab_path + ".tmp", 'w') as f_tmp:
|
||||
f_tmp.writelines(new_lines)
|
||||
os.rename(fstab_path + ".tmp", fstab_path)
|
||||
logger.info(f"已从 {fstab_path} 中移除 UUID={uuid} 的条目。")
|
||||
# Use sed for robust removal with sudo
|
||||
# suppress_critical_dialog_on_stderr_match is added to handle cases where fstab might not exist
|
||||
# or sed reports no changes, which is not a critical error for removal.
|
||||
command = ["sed", "-i", f"/UUID={uuid}/d", fstab_path]
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
command,
|
||||
f"从 {fstab_path} 中删除 UUID={uuid} 的条目失败",
|
||||
root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=(
|
||||
f"sed: {fstab_path}: No such file or directory", # English
|
||||
f"sed: {fstab_path}: 没有那个文件或目录", # Chinese
|
||||
"no changes were made" # if sed finds nothing to delete
|
||||
)
|
||||
)
|
||||
if success:
|
||||
logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"fstab 中未找到 UUID={uuid} 的条目。")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"修改 {fstab_path} 失败: {e}")
|
||||
QMessageBox.critical(None, "错误", f"修改 {fstab_path} 失败: {e}")
|
||||
return False
|
||||
# If sed failed, it might be because the entry wasn't found, which is fine.
|
||||
# _execute_shell_command would have suppressed the dialog if it matched the suppress_critical_dialog_on_stderr_match.
|
||||
# So, if we reach here, it's either a real error (dialog shown by _execute_shell_command)
|
||||
# or a suppressed "no changes" type of error. In both cases, if no real error, we return True.
|
||||
if any(s in stderr for s in (
|
||||
f"sed: {fstab_path}: No such file or directory",
|
||||
f"sed: {fstab_path}: 没有那个文件或目录",
|
||||
"no changes were made",
|
||||
"No such file or directory" # more general check
|
||||
)):
|
||||
logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。")
|
||||
return True # Consider it a success if the entry is not there
|
||||
return False # Other errors are already handled by _execute_shell_command
|
||||
|
||||
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
||||
"""
|
||||
@@ -181,18 +190,18 @@ class DiskOperations:
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
||||
if add_to_fstab:
|
||||
# 为了获取 fstype 和 UUID,需要再次调用 lsblk
|
||||
success_details, stdout_details, stderr_details = self._execute_shell_command(
|
||||
["lsblk", "-no", "FSTYPE,UUID", device_path],
|
||||
f"获取设备 {device_path} 的文件系统类型和 UUID 失败",
|
||||
root_privilege=False,
|
||||
suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address")
|
||||
)
|
||||
if success_details:
|
||||
fstype, uuid = stdout_details.strip().split()
|
||||
# NEW: 使用 self.system_manager 获取 fstype 和 UUID
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
if device_details:
|
||||
fstype = device_details.get('fstype')
|
||||
uuid = device_details.get('uuid')
|
||||
if fstype and uuid:
|
||||
self._add_to_fstab(device_path, mount_point, fstype, uuid)
|
||||
else:
|
||||
logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab: {stderr_details}")
|
||||
logger.error(f"无法获取设备 {device_path} 的文件系统类型或 UUID 以添加到 fstab。")
|
||||
QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取文件系统类型或 UUID 以添加到 fstab。")
|
||||
else:
|
||||
logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab。")
|
||||
QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。")
|
||||
return True
|
||||
else:
|
||||
@@ -206,23 +215,34 @@ class DiskOperations:
|
||||
:return: True 如果成功,否则 False。
|
||||
"""
|
||||
logger.info(f"尝试卸载设备 {device_path}。")
|
||||
try:
|
||||
# 定义表示设备已未挂载的错误信息(中英文)
|
||||
# 增加了 "未指定挂载点"
|
||||
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
|
||||
|
||||
# 调用 _execute_shell_command,并告诉它在遇到“已未挂载”错误时,不要弹出其自身的关键错误对话框。
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None
|
||||
suppress_critical_dialog_on_stderr_match=already_unmounted_errors
|
||||
)
|
||||
|
||||
if success:
|
||||
# 如果命令成功执行,则卸载成功。
|
||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
else:
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"卸载设备 {device_path} 时发生异常: {e}")
|
||||
if show_dialog_on_error:
|
||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 时发生异常: {e}")
|
||||
# 如果命令失败,检查是否是因为设备已经未挂载或挂载点未指定。
|
||||
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors)
|
||||
|
||||
if is_already_unmounted_error:
|
||||
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
|
||||
# 这种情况下,操作结果符合预期(设备已是未挂载),不弹出对话框,并返回 True。
|
||||
return True
|
||||
else:
|
||||
# 对于其他类型的卸载失败(例如设备忙、权限不足等),
|
||||
# _execute_shell_command 应该已经弹出了关键错误对话框
|
||||
# (因为它没有匹配到 `already_unmounted_errors` 进行抑制)。
|
||||
# 所以,这里我们不需要再次弹出对话框,直接返回 False。
|
||||
return False
|
||||
|
||||
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
|
||||
@@ -234,13 +254,19 @@ class DiskOperations:
|
||||
如果磁盘有分区表但没有空闲空间,返回 (None, None)。
|
||||
"""
|
||||
logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。")
|
||||
# suppress_critical_dialog_on_stderr_match is added to handle cases where parted might complain about
|
||||
# an unrecognized disk label, which is expected for a fresh disk.
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
|
||||
f"获取磁盘 {disk_path} 分区信息失败",
|
||||
root_privilege=True
|
||||
root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label")
|
||||
)
|
||||
|
||||
if not success:
|
||||
# If parted failed and it wasn't due to an unrecognized label (handled by suppress_critical_dialog_on_stderr_match),
|
||||
# then _execute_shell_command would have shown a dialog.
|
||||
# We just log and return None.
|
||||
logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}")
|
||||
return None, None
|
||||
|
||||
@@ -294,23 +320,31 @@ class DiskOperations:
|
||||
|
||||
# 1. 检查磁盘是否有分区表
|
||||
has_partition_table = False
|
||||
try:
|
||||
# Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking
|
||||
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "print"],
|
||||
f"检查磁盘 {disk_path} 分区表失败",
|
||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
||||
root_privilege=True
|
||||
)
|
||||
if success_check:
|
||||
# If success_check is False, it means _execute_shell_command encountered an error.
|
||||
# If that error was "unrecognized disk label", it was suppressed, and we treat it as no partition table.
|
||||
# If it was another error, a dialog was shown by _execute_shell_command, and we should stop.
|
||||
if not success_check:
|
||||
# Check if the failure was due to "unrecognized disk label" (which means no partition table)
|
||||
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")):
|
||||
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
|
||||
has_partition_table = False # Explicitly set to False
|
||||
else:
|
||||
logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}")
|
||||
return False # Other critical error, stop operation.
|
||||
else: # success_check is True
|
||||
if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check:
|
||||
has_partition_table = True
|
||||
else:
|
||||
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。")
|
||||
else:
|
||||
logger.info(f"parted print 命令失败,但可能不是因为没有分区表,错误信息: {stderr_check}")
|
||||
except Exception as e:
|
||||
logger.error(f"检查磁盘 {disk_path} 分区表时发生异常: {e}")
|
||||
pass
|
||||
has_partition_table = False
|
||||
|
||||
|
||||
actual_start_mib_for_parted = 0.0
|
||||
|
||||
@@ -374,11 +408,11 @@ class DiskOperations:
|
||||
create_cmd,
|
||||
f"在 {disk_path} 上创建分区失败"
|
||||
)
|
||||
if not success:
|
||||
return False
|
||||
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def delete_partition(self, device_path):
|
||||
"""
|
||||
@@ -394,9 +428,12 @@ class DiskOperations:
|
||||
return False
|
||||
|
||||
# 尝试卸载分区
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载
|
||||
# The unmount_partition method now handles "not mounted" gracefully without dialog and returns True.
|
||||
# So, we just call it.
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False) # show_dialog_on_error=False means no dialog for other errors too.
|
||||
|
||||
# 从 fstab 中移除条目
|
||||
# _remove_fstab_entry also handles "not found" gracefully.
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
# 获取父磁盘和分区号
|
||||
@@ -472,4 +509,3 @@ class DiskOperations:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
20
form.ui
20
form.ui
@@ -14,7 +14,14 @@
|
||||
<string>Linux 存储管理工具</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,4,1">
|
||||
<item>
|
||||
<widget class="QPushButton" name="refreshButton">
|
||||
<property name="text">
|
||||
<string>刷新数据</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
@@ -24,7 +31,7 @@
|
||||
<attribute name="title">
|
||||
<string>块设备概览</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="treeWidget_block_devices">
|
||||
<column>
|
||||
@@ -130,13 +137,6 @@
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="refreshButton">
|
||||
<property name="text">
|
||||
<string>刷新数据</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="logOutputTextEdit">
|
||||
<property name="readOnly">
|
||||
@@ -152,7 +152,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1000</width>
|
||||
<height>23</height>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
|
||||
@@ -35,7 +35,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 初始化管理器和操作类
|
||||
self.system_manager = SystemInfoManager()
|
||||
self.disk_ops = DiskOperations()
|
||||
self.disk_ops = DiskOperations(self.system_manager)
|
||||
self.raid_ops = RaidOperations()
|
||||
self.lvm_ops = LvmOperations()
|
||||
|
||||
@@ -208,12 +208,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if dev_data.get('children'):
|
||||
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
|
||||
calculated_start_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path)
|
||||
if calculated_start_mib is None:
|
||||
calculated_start_mib, largest_free_space_mib = self.disk_ops.get_disk_free_space_info_mib(disk_path, total_disk_mib)
|
||||
if calculated_start_mib is None or largest_free_space_mib is None:
|
||||
QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
||||
return
|
||||
start_position_mib = calculated_start_mib
|
||||
max_available_mib = total_disk_mib - start_position_mib
|
||||
max_available_mib = largest_free_space_mib
|
||||
if max_available_mib < 0:
|
||||
max_available_mib = 0.0
|
||||
else:
|
||||
|
||||
@@ -39,17 +39,18 @@ class RaidOperations:
|
||||
|
||||
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"标准错误: {e.stderr.strip()}")
|
||||
logger.error(f"标准错误: {stderr_output}")
|
||||
|
||||
# 根据 suppress_critical_dialog_on_stderr_match 参数决定是否弹出错误对话框
|
||||
if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in e.stderr.strip():
|
||||
if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in stderr_output:
|
||||
logger.info(f"特定错误 '{suppress_critical_dialog_on_stderr_match}' 匹配,已抑制错误对话框。")
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {e.stderr.strip()}")
|
||||
return False, e.stdout, e.stderr
|
||||
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
|
||||
return False, e.stdout, stderr_output # 返回 stderr_output
|
||||
except FileNotFoundError:
|
||||
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
@@ -59,13 +60,50 @@ class RaidOperations:
|
||||
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def _get_next_available_md_device_name(self):
|
||||
"""
|
||||
查找下一个可用的 /dev/mdX 设备名称(例如 /dev/md0, /dev/md1, ...)。
|
||||
"""
|
||||
existing_md_numbers = set()
|
||||
try:
|
||||
raid_arrays = self.system_manager.get_mdadm_arrays()
|
||||
for array in raid_arrays:
|
||||
device_path = array.get('device')
|
||||
if device_path and device_path.startswith('/dev/md'):
|
||||
# 匹配 /dev/mdX 形式的设备名
|
||||
match = re.match(r'/dev/md(\d+)', device_path)
|
||||
if match:
|
||||
existing_md_numbers.add(int(match.group(1)))
|
||||
except Exception as e:
|
||||
logger.warning(f"获取现有 RAID 阵列信息失败,可能无法找到最优的下一个设备名: {e}")
|
||||
|
||||
next_md_num = 0
|
||||
while next_md_num in existing_md_numbers:
|
||||
next_md_num += 1
|
||||
|
||||
return f"/dev/md{next_md_num}"
|
||||
|
||||
def create_raid_array(self, devices, level, chunk_size):
|
||||
"""
|
||||
创建 RAID 阵列。
|
||||
:param devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1']
|
||||
:param level: RAID 级别,例如 'raid0', 'raid1', 'raid5'
|
||||
: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):
|
||||
if level in ["0", "1", "5", "6", "10"]: # 增加 "6", "10" 确保全面
|
||||
level_str = f"raid{level}"
|
||||
else:
|
||||
level_str = level
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
|
||||
return False
|
||||
level = level_str
|
||||
# --- 标准化 RAID 级别输入结束 ---
|
||||
|
||||
if not devices or len(devices) < 2:
|
||||
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要两个成员设备。")
|
||||
return False
|
||||
@@ -74,6 +112,17 @@ class RaidOperations:
|
||||
if level == "raid5" and len(devices) < 3:
|
||||
QMessageBox.critical(None, "错误", "RAID5 至少需要三个设备。")
|
||||
return False
|
||||
# 补充其他 RAID 级别设备数量检查,与 dialogs.py 保持一致
|
||||
if level == "raid1" and len(devices) < 2:
|
||||
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
|
||||
return False
|
||||
if level == "raid6" and len(devices) < 4:
|
||||
QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。")
|
||||
return False
|
||||
if level == "raid10" and len(devices) < 2:
|
||||
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
|
||||
return False
|
||||
|
||||
|
||||
# 确认操作
|
||||
reply = QMessageBox.question(None, "确认创建 RAID 阵列",
|
||||
@@ -90,69 +139,80 @@ class RaidOperations:
|
||||
for dev in devices:
|
||||
# 使用 --force 选项,避免交互式提示
|
||||
clear_cmd = ["mdadm", "--zero-superblock", "--force", dev]
|
||||
|
||||
# 定义表示设备上没有超级块的错误信息,根据日志调整
|
||||
no_superblock_error_match = "Unrecognised md component device"
|
||||
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
clear_cmd,
|
||||
f"清除设备 {dev} 上的旧 RAID 超级块失败",
|
||||
suppress_critical_dialog_on_stderr_match="No superblocks" # 抑制“没有超级块”的错误提示
|
||||
suppress_critical_dialog_on_stderr_match=no_superblock_error_match
|
||||
)
|
||||
if success:
|
||||
|
||||
# 检查 stderr 是否包含“未识别的 MD 组件设备”信息
|
||||
if no_superblock_error_match in stderr:
|
||||
logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。")
|
||||
elif success:
|
||||
logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。")
|
||||
else:
|
||||
# 如果清除失败,但不是因为“没有超级块”,则可能需要进一步处理
|
||||
if "No superblocks" not in stderr:
|
||||
QMessageBox.warning(None, "警告", f"清除设备 {dev} 上的旧 RAID 超级块可能失败,但尝试继续。")
|
||||
logger.error(f"清除设备 {dev} 上的旧 RAID 超级块失败,中断 RAID 创建。")
|
||||
return False
|
||||
|
||||
# 2. 创建 RAID 阵列
|
||||
# 默认阵列名为 /dev/md/new_raid,可以考虑让用户输入
|
||||
array_name = "/dev/md/new_raid"
|
||||
# --- 修改点:自动生成唯一的阵列名称 ---
|
||||
array_name = self._get_next_available_md_device_name()
|
||||
logger.info(f"将使用自动生成的 RAID 阵列名称: {array_name}")
|
||||
# --- 修改点结束 ---
|
||||
|
||||
if level == "raid0":
|
||||
# RAID0 至少2个设备
|
||||
create_cmd = ["mdadm", "--create", array_name, "--level=raid0",
|
||||
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
|
||||
elif level == "raid1":
|
||||
# RAID1 至少2个设备
|
||||
create_cmd = ["mdadm", "--create", array_name, "--level=raid1",
|
||||
f"--raid-devices={len(devices)}"] + devices
|
||||
elif level == "raid5":
|
||||
# RAID5 至少3个设备
|
||||
create_cmd = ["mdadm", "--create", array_name, "--level=raid5",
|
||||
f"--raid-devices={len(devices)}"] + devices
|
||||
elif level == "raid6": # 增加 RAID6
|
||||
create_cmd = ["mdadm", "--create", array_name, "--level=raid6",
|
||||
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
|
||||
elif level == "raid10": # 增加 RAID10
|
||||
create_cmd = ["mdadm", "--create", array_name, "--level=raid10",
|
||||
f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices
|
||||
else:
|
||||
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别: {level}")
|
||||
return False
|
||||
|
||||
# 在 --create 命令中添加 --force 选项
|
||||
create_cmd.insert(2, "--force") # 插入到 --create 后面
|
||||
create_cmd.insert(2, "--force")
|
||||
|
||||
# <--- 关键修改:通过 input_to_command 参数传入 'y\n' 来强制 mdadm 接受
|
||||
if not self._execute_shell_command(create_cmd, f"创建 RAID 阵列失败", input_to_command='y\n')[0]:
|
||||
success_create, stdout_create, stderr_create = self._execute_shell_command(
|
||||
create_cmd,
|
||||
f"创建 RAID 阵列失败",
|
||||
input_to_command='y\n' # 尝试通过 stdin 传递 'y'
|
||||
)
|
||||
if not success_create:
|
||||
# 检查是否是由于 "Array name ... is in use already." 导致的失败
|
||||
if "Array name" in stderr_create and "is in use already" in stderr_create:
|
||||
QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
|
||||
return False
|
||||
|
||||
logger.info(f"成功创建 RAID {level} 阵列 {array_name}。")
|
||||
QMessageBox.information(None, "成功", f"成功创建 RAID {level} 阵列 {array_name}。")
|
||||
|
||||
# 3. 刷新 mdadm 配置并等待阵列激活
|
||||
# 注意:这里使用 '>>' 重定向,subprocess.run 无法直接处理 shell 重定向符号
|
||||
# 需要改成先读取,再写入,或者使用 bash -c "..."
|
||||
# 暂时先用 bash -c 的方式,更简单
|
||||
# 旧代码:self._execute_shell_command(["mdadm", "--examine", "--scan", ">>", "/etc/mdadm/mdadm.conf"], "更新 mdadm.conf 失败")
|
||||
|
||||
# 获取新的 mdadm.conf 内容
|
||||
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
|
||||
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
|
||||
if success_scan:
|
||||
# 将扫描结果追加到 mdadm.conf
|
||||
append_to_conf_cmd = ["bash", "-c", f"echo '{scan_stdout.strip()}' >> /etc/mdadm/mdadm.conf"]
|
||||
if not self._execute_shell_command(append_to_conf_cmd, "更新 /etc/mdadm/mdadm.conf 失败")[0]:
|
||||
logger.warning("更新 /etc/mdadm/mdadm.conf 失败。")
|
||||
else:
|
||||
logger.warning("未能扫描到 mdadm 配置,跳过更新 mdadm.conf。")
|
||||
|
||||
|
||||
# self._execute_shell_command(["update-initramfs", "-u"], "更新 initramfs 失败")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stop_raid_array(self, array_path):
|
||||
"""
|
||||
停止一个 RAID 阵列。
|
||||
@@ -173,7 +233,9 @@ class RaidOperations:
|
||||
# 暂时先直接调用 umount 命令,不处理 fstab
|
||||
# 或者,更好的方式是让 MainWindow 调用 disk_ops.unmount_partition
|
||||
# 此处简化处理,只执行 umount 命令
|
||||
self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", suppress_critical_dialog_on_stderr_match="not mounted")
|
||||
# 这里的 suppress_critical_dialog_on_stderr_match 应该与 DiskOperations 中的定义保持一致
|
||||
self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败",
|
||||
suppress_critical_dialog_on_stderr_match="not mounted") # 仅抑制英文,因为这里没有访问 DiskOperations 的 already_unmounted_errors
|
||||
|
||||
if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]:
|
||||
return False
|
||||
@@ -209,9 +271,24 @@ class RaidOperations:
|
||||
for dev in member_devices:
|
||||
# 使用 --force 选项,避免交互式提示
|
||||
clear_cmd = ["mdadm", "--zero-superblock", "--force", dev]
|
||||
if not self._execute_shell_command(clear_cmd, f"清除设备 {dev} 上的 RAID 超级块失败")[0]:
|
||||
|
||||
# 定义表示设备上没有超级块的错误信息,根据日志调整
|
||||
no_superblock_error_match = "Unrecognised md component device"
|
||||
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
clear_cmd,
|
||||
f"清除设备 {dev} 上的 RAID 超级块失败",
|
||||
suppress_critical_dialog_on_stderr_match=no_superblock_error_match
|
||||
)
|
||||
|
||||
if no_superblock_error_match in stderr:
|
||||
logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。")
|
||||
elif success:
|
||||
logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。")
|
||||
else:
|
||||
success_all_cleared = False
|
||||
QMessageBox.warning(None, "警告", f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。")
|
||||
# 错误对话框已由 _execute_shell_command 弹出,这里只记录警告
|
||||
logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。")
|
||||
|
||||
if success_all_cleared:
|
||||
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
|
||||
|
||||
218
system_info.py
218
system_info.py
@@ -3,7 +3,7 @@ import subprocess
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import os # 导入 os 模块
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -82,7 +82,7 @@ class SystemInfoManager:
|
||||
Helper to find device data by its path recursively.
|
||||
Added type check for target_path.
|
||||
"""
|
||||
if not isinstance(target_path, str): # 添加类型检查
|
||||
if not isinstance(target_path, str):
|
||||
logger.warning(f"传入 _find_device_by_path_recursive 的 target_path 不是字符串: {target_path} (类型: {type(target_path)})")
|
||||
return None
|
||||
for dev in dev_list:
|
||||
@@ -94,42 +94,109 @@ class SystemInfoManager:
|
||||
return found
|
||||
return None
|
||||
|
||||
def _find_device_by_maj_min_recursive(self, dev_list, target_maj_min):
|
||||
"""
|
||||
Helper to find device data by its major:minor number recursively.
|
||||
"""
|
||||
if not isinstance(target_maj_min, str):
|
||||
logger.warning(f"传入 _find_device_by_maj_min_recursive 的 target_maj_min 不是字符串: {target_maj_min} (类型: {type(target_maj_min)})")
|
||||
return None
|
||||
for dev in dev_list:
|
||||
if dev.get('maj:min') == target_maj_min:
|
||||
return dev
|
||||
if 'children' in dev:
|
||||
found = self._find_device_by_maj_min_recursive(dev['children'], target_maj_min)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
def get_mountpoint_for_device(self, device_path):
|
||||
"""
|
||||
根据设备路径获取其挂载点。
|
||||
此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。
|
||||
Added type check for device_path.
|
||||
此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。
|
||||
"""
|
||||
if not isinstance(device_path, str): # 添加类型检查
|
||||
if not isinstance(device_path, str):
|
||||
logger.warning(f"传入 get_mountpoint_for_device 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})")
|
||||
return None
|
||||
|
||||
original_device_path = device_path
|
||||
logger.debug(f"get_mountpoint_for_device: 正在处理原始设备路径: {original_device_path}")
|
||||
|
||||
devices = self.get_block_devices()
|
||||
logger.debug(f"get_mountpoint_for_device: 从 lsblk 获取到 {len(devices)} 个块设备信息。")
|
||||
|
||||
target_maj_min = None
|
||||
current_search_path = original_device_path
|
||||
|
||||
# 1. 解析符号链接以获取实际路径,并尝试获取 maj:min
|
||||
if original_device_path.startswith('/dev/') and os.path.exists(original_device_path):
|
||||
try:
|
||||
# 先尝试解析真实路径,因为 stat 可能对符号链接返回链接本身的 maj:min
|
||||
resolved_path = os.path.realpath(original_device_path)
|
||||
logger.debug(f"get_mountpoint_for_device: 原始设备路径 {original_device_path} 解析为 {resolved_path}")
|
||||
current_search_path = resolved_path # 使用解析后的路径进行 stat 和后续查找
|
||||
|
||||
# 获取解析后路径的 maj:min
|
||||
stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True)
|
||||
raw_maj_min = stat_output.strip() # e.g., "fc:0"
|
||||
|
||||
# MODIFIED: Convert hex major to decimal
|
||||
if ':' in raw_maj_min:
|
||||
major_hex, minor_dec = raw_maj_min.split(':')
|
||||
try:
|
||||
major_dec = str(int(major_hex, 16)) # Convert hex to decimal string
|
||||
target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0"
|
||||
logger.debug(f"get_mountpoint_for_device: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}")
|
||||
except ValueError:
|
||||
logger.warning(f"get_mountpoint_for_device: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。")
|
||||
target_maj_min = None # Fallback if conversion fails
|
||||
else:
|
||||
logger.warning(f"get_mountpoint_for_device: stat 输出 '{raw_maj_min}' 格式不符合预期。")
|
||||
target_maj_min = None
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}")
|
||||
except Exception as e:
|
||||
logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}")
|
||||
elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path):
|
||||
logger.warning(f"get_mountpoint_for_device: 设备路径 {original_device_path} 不存在,无法获取挂载点。")
|
||||
return None
|
||||
|
||||
# 2. 尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找
|
||||
dev_info = self._find_device_by_path_recursive(devices, current_search_path)
|
||||
logger.debug(f"get_mountpoint_for_device: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}): {dev_info}")
|
||||
|
||||
# 1. 尝试直接使用提供的 device_path 在 lsblk 输出中查找
|
||||
dev_info = self._find_device_by_path_recursive(devices, device_path)
|
||||
if dev_info:
|
||||
logger.debug(f"直接从 lsblk 获取到 {device_path} 的挂载点: {dev_info.get('mountpoint')}")
|
||||
return dev_info.get('mountpoint')
|
||||
mountpoint = dev_info.get('mountpoint')
|
||||
logger.debug(f"get_mountpoint_for_device: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的挂载点: {mountpoint}")
|
||||
return mountpoint
|
||||
|
||||
# 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid)
|
||||
if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查
|
||||
logger.debug(f"处理 RAID 阵列 {device_path} 以获取挂载点...")
|
||||
# 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找
|
||||
if target_maj_min:
|
||||
logger.debug(f"get_mountpoint_for_device: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。")
|
||||
dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min)
|
||||
logger.debug(f"get_mountpoint_for_device: 通过 maj:min 查找结果: {dev_info_by_maj_min}")
|
||||
if dev_info_by_maj_min:
|
||||
mountpoint = dev_info_by_maj_min.get('mountpoint')
|
||||
logger.debug(f"get_mountpoint_for_device: 通过 maj:min 找到 {original_device_path} 的挂载点: {mountpoint}")
|
||||
return mountpoint
|
||||
|
||||
# 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127)
|
||||
actual_md_device_path = self._get_actual_md_device_path(device_path)
|
||||
logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}")
|
||||
# 4. 如果仍然查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid)
|
||||
if original_device_path.startswith('/dev/md'):
|
||||
logger.debug(f"get_mountpoint_for_device: 正在处理 RAID 阵列 {original_device_path} 以获取挂载点...")
|
||||
actual_md_device_path = self._get_actual_md_device_path(original_device_path)
|
||||
logger.debug(f"get_mountpoint_for_device: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}")
|
||||
|
||||
if actual_md_device_path:
|
||||
# 现在,使用实际的内核设备路径从 lsblk 中查找挂载点
|
||||
actual_dev_info = self._find_device_by_path_recursive(devices, actual_md_device_path)
|
||||
logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}")
|
||||
logger.debug(f"get_mountpoint_for_device: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}")
|
||||
|
||||
if actual_dev_info:
|
||||
logger.debug(f"在实际设备 {actual_md_device_path} 上找到了挂载点: {actual_dev_info.get('mountpoint')}")
|
||||
return actual_dev_info.get('mountpoint')
|
||||
mountpoint = actual_dev_info.get('mountpoint')
|
||||
logger.debug(f"get_mountpoint_for_device: 在实际设备 {actual_md_device_path} 上找到了挂载点: {mountpoint}")
|
||||
return mountpoint
|
||||
|
||||
logger.debug(f"未能获取到 {device_path} 的挂载点。")
|
||||
logger.debug(f"get_mountpoint_for_device: 未能获取到 {original_device_path} 的挂载点。")
|
||||
return None
|
||||
|
||||
def get_mdadm_arrays(self):
|
||||
@@ -269,9 +336,10 @@ class SystemInfoManager:
|
||||
except Exception as e:
|
||||
logger.error(f"获取LVM卷组信息失败: {e}")
|
||||
|
||||
# Get LVs
|
||||
# Get LVs (MODIFIED: added -o lv_path)
|
||||
try:
|
||||
stdout, _ = self._run_command(["lvs", "--reportformat", "json"])
|
||||
# 明确请求 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"])
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
for lv_data in data['report'][0].get('lv', []):
|
||||
@@ -283,7 +351,7 @@ class SystemInfoManager:
|
||||
'lv_attr': lv_data.get('lv_attr'),
|
||||
'origin': lv_data.get('origin'),
|
||||
'snap_percent': lv_data.get('snap_percent'),
|
||||
'lv_path': lv_data.get('lv_path')
|
||||
'lv_path': lv_data.get('lv_path') # 现在应该能正确获取到路径了
|
||||
})
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout:
|
||||
@@ -358,7 +426,7 @@ class SystemInfoManager:
|
||||
通过检查 /dev/md/ 目录下的符号链接来获取。
|
||||
Added type check for array_path.
|
||||
"""
|
||||
if not isinstance(array_path, str): # 添加类型检查
|
||||
if not isinstance(array_path, str):
|
||||
logger.warning(f"传入 _get_actual_md_device_path 的 array_path 不是字符串: {array_path} (类型: {type(array_path)})")
|
||||
return None
|
||||
|
||||
@@ -378,63 +446,103 @@ class SystemInfoManager:
|
||||
def get_device_details_by_path(self, device_path):
|
||||
"""
|
||||
根据设备路径获取设备的 UUID 和文件系统类型 (fstype)。
|
||||
此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。
|
||||
此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。
|
||||
Added type check for device_path.
|
||||
:param device_path: 设备的路径,例如 '/dev/sdb1' 或 '/dev/md0' 或 '/dev/md/new_raid'
|
||||
:return: 包含 'uuid' 和 'fstype' 的字典,如果未找到则返回 None。
|
||||
"""
|
||||
if not isinstance(device_path, str): # 添加类型检查
|
||||
if not isinstance(device_path, str):
|
||||
logger.warning(f"传入 get_device_details_by_path 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})")
|
||||
return None
|
||||
|
||||
original_device_path = device_path
|
||||
logger.debug(f"get_device_details_by_path: 正在处理原始设备路径: {original_device_path}")
|
||||
|
||||
devices = self.get_block_devices() # 获取所有块设备信息
|
||||
logger.debug(f"get_device_details_by_path: 从 lsblk 获取到 {len(devices)} 个块设备信息。")
|
||||
|
||||
# 1. 首先,尝试直接使用提供的 device_path 在 lsblk 输出中查找
|
||||
# 对于 /dev/md/new_raid 这样的别名,这一步通常会返回 None
|
||||
lsblk_details = self._find_device_by_path_recursive(devices, device_path)
|
||||
logger.debug(f"lsblk_details for {device_path} (direct lookup): {lsblk_details}")
|
||||
target_maj_min = None
|
||||
current_search_path = original_device_path
|
||||
|
||||
if lsblk_details and lsblk_details.get('fstype'): # 即使没有UUID,有fstype也算找到部分信息
|
||||
# 如果直接找到了 fstype,就返回 lsblk 提供的 UUID 和 fstype
|
||||
# 这里的 UUID 应该是文件系统 UUID
|
||||
logger.debug(f"直接从 lsblk 获取到 {device_path} 的详情: {lsblk_details}")
|
||||
# 1. 解析符号链接以获取实际路径,并尝试获取 maj:min
|
||||
if original_device_path.startswith('/dev/') and os.path.exists(original_device_path):
|
||||
try:
|
||||
resolved_path = os.path.realpath(original_device_path)
|
||||
logger.debug(f"get_device_details_by_path: 原始设备路径 {original_device_path} 解析为 {resolved_path}")
|
||||
current_search_path = resolved_path
|
||||
|
||||
stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True)
|
||||
raw_maj_min = stat_output.strip() # e.g., "fc:0"
|
||||
|
||||
# MODIFIED: Convert hex major to decimal
|
||||
if ':' in raw_maj_min:
|
||||
major_hex, minor_dec = raw_maj_min.split(':')
|
||||
try:
|
||||
major_dec = str(int(major_hex, 16)) # Convert hex to decimal string
|
||||
target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0"
|
||||
logger.debug(f"get_device_details_by_path: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}")
|
||||
except ValueError:
|
||||
logger.warning(f"get_device_details_by_path: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。")
|
||||
target_maj_min = None
|
||||
else:
|
||||
logger.warning(f"get_device_details_by_path: stat 输出 '{raw_maj_min}' 格式不符合预期。")
|
||||
target_maj_min = None
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}")
|
||||
except Exception as e:
|
||||
logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}")
|
||||
elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path):
|
||||
logger.warning(f"get_device_details_by_path: 设备路径 {original_device_path} 不存在,无法获取详情。")
|
||||
return None
|
||||
|
||||
# 2. 首先,尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找
|
||||
lsblk_details = self._find_device_by_path_recursive(devices, current_search_path)
|
||||
logger.debug(f"get_device_details_by_path: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}, 直接查找): {lsblk_details}")
|
||||
|
||||
if lsblk_details and lsblk_details.get('fstype'):
|
||||
logger.debug(f"get_device_details_by_path: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的详情: {lsblk_details}")
|
||||
return {
|
||||
'uuid': lsblk_details.get('uuid'),
|
||||
'fstype': lsblk_details.get('fstype')
|
||||
}
|
||||
|
||||
# 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid)
|
||||
if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查
|
||||
logger.debug(f"处理 RAID 阵列 {device_path}...")
|
||||
# 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找
|
||||
if target_maj_min:
|
||||
logger.debug(f"get_device_details_by_path: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。")
|
||||
dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min)
|
||||
logger.debug(f"get_device_details_by_path: 通过 maj:min 查找结果: {dev_info_by_maj_min}")
|
||||
if dev_info_by_maj_min and dev_info_by_maj_min.get('fstype'):
|
||||
logger.debug(f"get_device_details_by_path: 通过 maj:min 找到 {original_device_path} 的文件系统详情: {dev_info_by_maj_min}")
|
||||
return {
|
||||
'uuid': dev_info_by_maj_min.get('uuid'),
|
||||
'fstype': dev_info_by_maj_min.get('fstype')
|
||||
}
|
||||
|
||||
# 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127)
|
||||
actual_md_device_path = self._get_actual_md_device_path(device_path)
|
||||
logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}")
|
||||
# 4. 如果仍然没有找到,并且是 RAID 阵列(例如 /dev/md/new_raid)
|
||||
if original_device_path.startswith('/dev/md'):
|
||||
logger.debug(f"get_device_details_by_path: 正在处理 RAID 阵列 {original_device_path}...")
|
||||
|
||||
actual_md_device_path = self._get_actual_md_device_path(original_device_path)
|
||||
logger.debug(f"get_device_details_by_path: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}")
|
||||
|
||||
if actual_md_device_path:
|
||||
# 现在,使用实际的内核设备路径从 lsblk 中查找 fstype 和 UUID (文件系统 UUID)
|
||||
actual_device_lsblk_details = self._find_device_by_path_recursive(devices, actual_md_device_path)
|
||||
logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}")
|
||||
logger.debug(f"get_device_details_by_path: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}")
|
||||
|
||||
if actual_device_lsblk_details and actual_device_lsblk_details.get('fstype'):
|
||||
# 找到了实际 RAID 设备上的文件系统信息
|
||||
# 此时的 UUID 是文件系统 UUID,fstype 是文件系统类型
|
||||
logger.debug(f"在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}")
|
||||
logger.debug(f"get_device_details_by_path: 在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}")
|
||||
return {
|
||||
'uuid': actual_device_lsblk_details.get('uuid'),
|
||||
'fstype': actual_device_lsblk_details.get('fstype')
|
||||
}
|
||||
else:
|
||||
# RAID 设备存在,但 lsblk 没有报告文件系统 (例如,尚未格式化)
|
||||
# 此时 fstype 为 None。如果需要,我们可以返回 RAID 阵列本身的 UUID,但 fstype 仍为 None
|
||||
logger.warning(f"RAID 阵列 {device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。")
|
||||
# 对于 fstab,如果没有 fstype,就无法创建条目。
|
||||
# 此时返回 None,让调用者知道无法写入 fstab。
|
||||
logger.warning(f"get_device_details_by_path: RAID 阵列 {original_device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。")
|
||||
return None
|
||||
else:
|
||||
logger.warning(f"无法确定 RAID 阵列 {device_path} 的实际内核设备路径。")
|
||||
return None # 无法解析实际设备路径,也无法获取 fstype
|
||||
|
||||
# 3. 如果仍然没有找到,返回 None
|
||||
logger.debug(f"未能获取到 {device_path} 的任何详情。")
|
||||
logger.warning(f"get_device_details_by_path: 无法确定 RAID 阵列 {original_device_path} 的实际内核设备路径。")
|
||||
return None
|
||||
|
||||
# 5. 如果仍然没有找到,返回 None
|
||||
logger.debug(f"get_device_details_by_path: 未能获取到 {original_device_path} 的任何详情。")
|
||||
return None
|
||||
|
||||
30
ui_form.py
30
ui_form.py
@@ -27,18 +27,23 @@ class Ui_MainWindow(object):
|
||||
MainWindow.resize(1000, 700)
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.verticalLayout = QVBoxLayout(self.centralwidget)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.verticalLayout_2 = QVBoxLayout(self.centralwidget)
|
||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||
self.refreshButton = QPushButton(self.centralwidget)
|
||||
self.refreshButton.setObjectName(u"refreshButton")
|
||||
|
||||
self.verticalLayout_2.addWidget(self.refreshButton)
|
||||
|
||||
self.tabWidget = QTabWidget(self.centralwidget)
|
||||
self.tabWidget.setObjectName(u"tabWidget")
|
||||
self.tab_block_devices = QWidget()
|
||||
self.tab_block_devices.setObjectName(u"tab_block_devices")
|
||||
self.verticalLayout_2 = QVBoxLayout(self.tab_block_devices)
|
||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||
self.verticalLayout = QVBoxLayout(self.tab_block_devices)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.treeWidget_block_devices = QTreeWidget(self.tab_block_devices)
|
||||
self.treeWidget_block_devices.setObjectName(u"treeWidget_block_devices")
|
||||
|
||||
self.verticalLayout_2.addWidget(self.treeWidget_block_devices)
|
||||
self.verticalLayout.addWidget(self.treeWidget_block_devices)
|
||||
|
||||
self.tabWidget.addTab(self.tab_block_devices, "")
|
||||
self.tab_raid = QWidget()
|
||||
@@ -62,23 +67,20 @@ class Ui_MainWindow(object):
|
||||
|
||||
self.tabWidget.addTab(self.tab_lvm, "")
|
||||
|
||||
self.verticalLayout.addWidget(self.tabWidget)
|
||||
|
||||
self.refreshButton = QPushButton(self.centralwidget)
|
||||
self.refreshButton.setObjectName(u"refreshButton")
|
||||
|
||||
self.verticalLayout.addWidget(self.refreshButton)
|
||||
self.verticalLayout_2.addWidget(self.tabWidget)
|
||||
|
||||
self.logOutputTextEdit = QTextEdit(self.centralwidget)
|
||||
self.logOutputTextEdit.setObjectName(u"logOutputTextEdit")
|
||||
self.logOutputTextEdit.setReadOnly(True)
|
||||
|
||||
self.verticalLayout.addWidget(self.logOutputTextEdit)
|
||||
self.verticalLayout_2.addWidget(self.logOutputTextEdit)
|
||||
|
||||
self.verticalLayout_2.setStretch(1, 4)
|
||||
self.verticalLayout_2.setStretch(2, 1)
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1000, 23))
|
||||
self.menubar.setGeometry(QRect(0, 0, 1000, 30))
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.statusbar = QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
@@ -94,6 +96,7 @@ class Ui_MainWindow(object):
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Linux \u5b58\u50a8\u7ba1\u7406\u5de5\u5177", None))
|
||||
self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None))
|
||||
___qtreewidgetitem = self.treeWidget_block_devices.headerItem()
|
||||
___qtreewidgetitem.setText(12, QCoreApplication.translate("MainWindow", u"\u7236\u8bbe\u5907\u540d", None));
|
||||
___qtreewidgetitem.setText(11, QCoreApplication.translate("MainWindow", u"\u4e3b\u6b21\u53f7", None));
|
||||
@@ -115,6 +118,5 @@ class Ui_MainWindow(object):
|
||||
___qtreewidgetitem2 = self.treeWidget_lvm.headerItem()
|
||||
___qtreewidgetitem2.setText(0, QCoreApplication.translate("MainWindow", u"1", None));
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_lvm), QCoreApplication.translate("MainWindow", u"LVM \u7ba1\u7406", None))
|
||||
self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user