This commit is contained in:
zj
2026-02-02 18:38:41 +08:00
parent be5a0bc2c7
commit a90725d178
14 changed files with 2345 additions and 465 deletions

View File

@@ -1,8 +1,9 @@
# system_info.py
import subprocess
import json
import os
import logging
import re
import os # 导入 os 模块
logger = logging.getLogger(__name__)
@@ -10,255 +11,430 @@ class SystemInfoManager:
def __init__(self):
pass
def _run_command(self, cmd, root_privilege=False, check_output=True):
def _run_command(self, command_list, root_privilege=False, check_output=True, input_data=None):
"""
执行shell命令并返回stdout和stderr
如果需要root权限会尝试使用sudo
所有输出和错误都会通过 logger 记录
通用地运行一个 shell 命令。
:param command_list: 命令及其参数的列表
:param root_privilege: 如果为 True则使用 sudo 执行命令
:param check_output: 如果为 True则捕获 stdout 和 stderr。如果为 False则不捕获用于需要交互或不关心输出的命令。
:param input_data: 传递给命令stdin的数据 (str)。
:return: (stdout_str, stderr_str)
:raises subprocess.CalledProcessError: 如果命令返回非零退出码。
"""
full_cmd = []
if root_privilege:
# 检查当前是否已经是root用户
if os.geteuid() != 0:
full_cmd.append("sudo")
full_cmd.extend(cmd)
cmd_str = ' '.join(full_cmd)
logger.info(f"执行命令: {cmd_str}")
command_list = ["sudo"] + command_list
logger.debug(f"运行命令: {' '.join(command_list)}")
try:
result = subprocess.run(
full_cmd,
capture_output=True,
command_list,
capture_output=check_output,
text=True,
check=check_output,
check=True,
encoding='utf-8',
env=dict(os.environ, LANG="en_US.UTF-8")
input=input_data
)
if result.stdout:
logger.debug(f"命令输出 (stdout):\n{result.stdout.strip()}")
if result.stderr:
logger.warning(f"命令输出 (stderr):\n{result.stderr.strip()}")
return result.stdout.strip(), result.stderr.strip()
return result.stdout if check_output else "", result.stderr if check_output else ""
except subprocess.CalledProcessError as e:
error_msg = f"命令执行失败: {cmd_str}\n" \
f"退出码: {e.returncode}\n" \
f"标准输出: {e.stdout}\n" \
f"标准错误: {e.stderr}"
logger.error(error_msg)
raise ValueError(error_msg)
logger.error(f"命令执行失败: {' '.join(command_list)}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip()}")
logger.error(f"标准错误: {e.stderr.strip()}")
raise
except FileNotFoundError:
error_msg = f"命令未找到: {full_cmd[0]}。请确保已安装。"
logger.error(error_msg)
raise FileNotFoundError(error_msg)
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具")
raise
except Exception as e:
error_msg = f"行命令时发生未知错误: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg)
logger.error(f"行命令 {' '.join(command_list)} 时发生未知错误: {e}")
raise
def get_block_devices(self):
"""
获取所有块设备信息以JSON格式返回
使用 lsblk -J 命令
使用 lsblk 获取块设备信息。
返回一个字典列表,每个字典代表一个设备
"""
cmd = [
"lsblk", "-J", "-o",
"NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH"
]
try:
stdout, _ = self._run_command(["lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME"], root_privilege=False)
stdout, _ = self._run_command(cmd)
data = json.loads(stdout)
logger.info("成功获取块设备信息。")
return data.get('blockdevices', [])
devices = data.get('blockdevices', [])
def add_path_recursive(dev_list):
for dev in dev_list:
if 'PATH' not in dev and 'NAME' in dev:
dev['PATH'] = f"/dev/{dev['NAME']}"
# Rename PATH to path (lowercase) for consistency
if 'PATH' in dev:
dev['path'] = dev.pop('PATH')
if 'children' in dev:
add_path_recursive(dev['children'])
add_path_recursive(devices)
return devices
except Exception as e:
logger.error(f"获取块设备信息失败: {e}")
return []
def _find_device_by_path_recursive(self, dev_list, target_path):
"""
Helper to find device data by its path recursively.
Added type check for target_path.
"""
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:
if dev.get('path') == target_path:
return dev
if 'children' in dev:
found = self._find_device_by_path_recursive(dev['children'], target_path)
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.
"""
if not isinstance(device_path, str): # 添加类型检查
logger.warning(f"传入 get_mountpoint_for_device 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})")
return None
devices = self.get_block_devices()
# 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')
# 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid
if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查
logger.debug(f"处理 RAID 阵列 {device_path} 以获取挂载点...")
# 获取 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}")
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}")
if actual_dev_info:
logger.debug(f"在实际设备 {actual_md_device_path} 上找到了挂载点: {actual_dev_info.get('mountpoint')}")
return actual_dev_info.get('mountpoint')
logger.debug(f"未能获取到 {device_path} 的挂载点。")
return None
def get_mdadm_arrays(self):
"""
获取所有RAID阵列的详细信息。
首先使用 mdadm --detail --scan 获取阵列列表,
然后对每个阵列使用 mdadm --detail 获取更详细的信息。
获取 mdadm RAID 阵列信息。
"""
all_raid_details = []
cmd = ["mdadm", "--detail", "--scan"]
try:
# 1. 获取所有RAID设备的路径
scan_stdout, _ = self._run_command(["mdadm", "--detail", "--scan"], root_privilege=False)
array_paths = []
for line in scan_stdout.splitlines():
stdout, _ = self._run_command(cmd)
arrays = []
for line in stdout.splitlines():
if line.startswith("ARRAY"):
parts = line.split()
if len(parts) > 1:
array_paths.append(parts[1]) # 例如 /dev/md126
if not array_paths:
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 []
# 2. 对每个RAID设备获取详细信息
for path in array_paths:
try:
detail_stdout, _ = self._run_command(["mdadm", "--detail", path], root_privilege=False)
parsed_detail = self._parse_mdadm_detail_output(detail_stdout)
parsed_detail['device'] = path # 添加设备路径
all_raid_details.append(parsed_detail)
except Exception as e:
logger.error(f"获取RAID阵列 {path} 的详细信息失败: {e}")
logger.info("成功获取RAID阵列信息。")
return all_raid_details
logger.error(f"获取RAID阵列信息失败: {e}")
return []
except Exception as e:
logger.error(f"获取RAID阵列列表失败: {e}")
logger.error(f"获取RAID阵列信息失败: {e}")
return []
def _parse_mdadm_detail_output(self, output_string):
def _parse_mdadm_detail(self, detail_output, array_path):
"""
解析 mdadm --detail 命令的输出字符串,提取关键信息
解析 mdadm --detail 的输出。
"""
details = {
info = {
'device': array_path,
'level': 'N/A',
'array_size': 'N/A',
'raid_devices': 'N/A',
'total_devices': 'N/A',
'state': 'N/A',
'array_size': 'N/A',
'active_devices': 'N/A',
'working_devices': 'N/A',
'failed_devices': 'N/A',
'spare_devices': 'N/A',
'chunk_size': 'N/A',
'total_devices': 'N/A',
'uuid': 'N/A',
'name': 'N/A',
'chunk_size': 'N/A',
'member_devices': []
}
member_devices_section = False
lines = output_string.splitlines()
member_pattern = re.compile(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+?)\s+(/dev/.+)$')
for line in lines:
line = line.strip()
if not line:
continue
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()
if line.startswith("Number Major Minor RaidDevice State"):
member_devices_section = True
continue
if member_devices_section:
parts = line.split()
if len(parts) >= 6: # Ensure enough parts for device info
# State can be multiple words, e.g., "active sync"
device_state_parts = parts[4:-1]
# Handle cases where device path might have spaces or be complex, though usually not for /dev/sdX
device_path = parts[-1]
details['member_devices'].append({
'number': parts[0],
'major': parts[1],
'minor': parts[2],
'raid_device': parts[3],
'state': ' '.join(device_state_parts),
'device_path': device_path
})
else:
if ':' in line:
key, value = line.split(':', 1)
key = key.strip().lower().replace(' ', '_')
value = value.strip()
if key == 'raid_level':
details['level'] = value
elif key == 'array_size':
details['array_size'] = value
elif key == 'raid_devices':
details['raid_devices'] = value
elif key == 'total_devices':
details['total_devices'] = value
elif key == 'state':
details['state'] = value
elif key == 'active_devices':
details['active_devices'] = value
elif key == 'working_devices':
details['working_devices'] = value
elif key == 'failed_devices':
details['failed_devices'] = value
elif key == 'spare_devices':
details['spare_devices'] = value
elif key == 'chunk_size':
details['chunk_size'] = value
elif key == 'uuid':
details['uuid'] = value
elif key == 'name':
details['name'] = value
return details
# 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
def get_lvm_info(self):
"""
获取LVM物理卷、卷组、逻辑卷信息。
使用 pvs, vgs, lvs 命令并尝试获取JSON格式。
获取 LVM 物理卷 (PVs)、卷组 (VGs) 和逻辑卷 (LVs) 的信息。
"""
lvm_info = {}
try:
stdout_pvs, _ = self._run_command(["pvs", "--reportformat", "json"], root_privilege=False)
lvm_info['pvs'] = json.loads(stdout_pvs).get('report', [])[0].get('pv', [])
logger.info("成功获取物理卷信息。")
except Exception as e:
logger.error(f"获取物理卷信息失败: {e}")
lvm_info['pvs'] = []
lvm_info = {'pvs': [], 'vgs': [], 'lvs': []}
# Get PVs
try:
stdout_vgs, _ = self._run_command(["vgs", "--reportformat", "json"], root_privilege=False)
lvm_info['vgs'] = json.loads(stdout_vgs).get('report', [])[0].get('vg', [])
logger.info("成功获取卷组信息。")
stdout, _ = 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', []):
lvm_info['pvs'].append({
'pv_name': pv_data.get('pv_name'),
'vg_name': pv_data.get('vg_name'),
'pv_uuid': pv_data.get('pv_uuid'),
'pv_size': pv_data.get('pv_size'),
'pv_free': pv_data.get('pv_free'),
'pv_attr': pv_data.get('pv_attr'),
'pv_fmt': pv_data.get('pv_fmt')
})
except subprocess.CalledProcessError as e:
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}")
except Exception as e:
logger.error(f"获取卷信息失败: {e}")
lvm_info['vgs'] = []
logger.error(f"获取LVM物理卷信息失败: {e}")
# Get VGs
try:
stdout_lvs, _ = self._run_command(["lvs", "--reportformat", "json"], root_privilege=False)
lvm_info['lvs'] = json.loads(stdout_lvs).get('report', [])[0].get('lv', [])
logger.info("成功获取逻辑卷信息。")
stdout, _ = 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', []):
lvm_info['vgs'].append({
'vg_name': vg_data.get('vg_name'),
'vg_uuid': vg_data.get('vg_uuid'),
'vg_size': vg_data.get('vg_size'),
'vg_free': vg_data.get('vg_free'),
'vg_attr': vg_data.get('vg_attr'),
'pv_count': vg_data.get('pv_count'),
'lv_count': vg_data.get('lv_count'),
'vg_alloc_percent': vg_data.get('vg_alloc_percent'),
'vg_fmt': vg_data.get('vg_fmt')
})
except subprocess.CalledProcessError as e:
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}")
except Exception as e:
logger.error(f"获取逻辑卷信息失败: {e}")
lvm_info['lvs'] = []
logger.error(f"获取LVM卷组信息失败: {e}")
# Get LVs
try:
stdout, _ = self._run_command(["lvs", "--reportformat", "json"])
data = json.loads(stdout)
if 'report' in data and data['report']:
for lv_data in data['report'][0].get('lv', []):
lvm_info['lvs'].append({
'lv_name': lv_data.get('lv_name'),
'vg_name': lv_data.get('vg_name'),
'lv_uuid': lv_data.get('lv_uuid'),
'lv_size': lv_data.get('lv_size'),
'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')
})
except subprocess.CalledProcessError as e:
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}")
except Exception as e:
logger.error(f"获取LVM逻辑卷信息失败: {e}")
return lvm_info
# 示例用法 (可以在此模块中添加测试代码)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
manager = SystemInfoManager()
logger.info("--- 块设备信息 ---")
devices = manager.get_block_devices()
for dev in devices:
logger.info(f" NAME: {dev.get('name')}, TYPE: {dev.get('type')}, SIZE: {dev.get('size')}, MOUNTPOINT: {dev.get('mountpoint')}")
def get_unallocated_partitions(self):
"""
获取所有可用于创建RAID阵列或LVM物理卷的设备。
这些设备包括:
1. 没有分区表的整个磁盘。
2. 未挂载的分区包括有文件系统但未挂载的如ext4/xfs等
3. 未被LVM或RAID使用的分区或磁盘。
返回一个设备路径列表,例如 ['/dev/sdb1', '/dev/sdc']。
"""
block_devices = self.get_block_devices()
candidates = []
logger.info("\n--- RAID 阵列信息 ---")
raid_info = manager.get_mdadm_arrays()
if raid_info:
for array in raid_info:
logger.info(f" Device: {array.get('device')}, Level: {array.get('level')}, State: {array.get('state')}, Size: {array.get('array_size')}")
logger.info(f" Active: {array.get('active_devices')}, Failed: {array.get('failed_devices')}, Spare: {array.get('spare_devices')}")
for member in array.get('member_devices', []):
logger.info(f" Member: {member.get('device_path')} (State: {member.get('state')})")
else:
logger.info(" 未找到RAID阵列。")
def process_device(dev):
dev_path = dev.get('path')
dev_type = dev.get('type')
mountpoint = dev.get('mountpoint')
fstype = dev.get('fstype')
logger.info("\n--- LVM 信息 ---")
lvm_info = manager.get_lvm_info()
logger.info("物理卷 (PVs):")
if lvm_info['pvs']:
for pv in lvm_info['pvs']:
logger.info(f" {pv.get('pv_name')} (VG: {pv.get('vg_name')}, Size: {pv.get('pv_size')})")
else:
logger.info(" 未找到物理卷。")
# 1. 检查是否已挂载 (排除SWAP因为SWAP可以被覆盖但如果活跃则不应动)
# 如果是活跃挂载点非SWAP则不是候选
if mountpoint and mountpoint != '[SWAP]':
# 如果设备已挂载,则它不是候选。
# 但仍需处理其子设备,因为父设备挂载不代表子设备不能用(虽然不常见)
# 或者子设备挂载不代表父设备不能用(例如使用整个磁盘)
if 'children' in dev:
for child in dev['children']:
process_device(child)
return
logger.info("卷组 (VGs):")
if lvm_info['vgs']:
for vg in lvm_info['vgs']:
logger.info(f" {vg.get('vg_name')} (Size: {vg.get('vg_size')}, PVs: {vg.get('pv_count')}, LVs: {vg.get('lv_count')})")
else:
logger.info(" 未找到卷组。")
# 2. 检查是否是LVM物理卷或RAID成员
# 如果是LVM PV或RAID成员则它不是新RAID/PV的候选
if fstype in ['LVM2_member', 'linux_raid_member']:
if 'children' in dev: # 即使是LVM/RAID成员也可能存在子设备例如LVM上的分区虽然不常见
for child in dev['children']:
process_device(child)
return
logger.info("逻辑卷 (LVs):")
if lvm_info['lvs']:
for lv in lvm_info['lvs']:
logger.info(f" {lv.get('lv_name')} (VG: {lv.get('vg_name')}, Size: {lv.get('lv_size')})")
else:
logger.info(" 未找到逻辑卷。")
# 3. 如果是整个磁盘且没有分区,则是一个候选
if dev_type == 'disk' and not dev.get('children'):
candidates.append(dev_path)
# 4. 如果是分区且通过了上述挂载和LVM/RAID检查则是一个候选
elif dev_type == 'part':
candidates.append(dev_path)
# 5. 递归处理子设备 (对于有分区的磁盘)
# 这一步放在最后,确保先对当前设备进行评估
if dev_type == 'disk' and dev.get('children'): # 只有当是磁盘且有子设备时才需要递归
for child in dev['children']:
process_device(child)
for dev in block_devices:
process_device(dev)
# 去重并排序
return sorted(list(set(candidates)))
def _get_actual_md_device_path(self, array_path):
"""
解析 mdadm 阵列名称(例如 /dev/md/new_raid
对应的实际内核设备路径(例如 /dev/md127
通过检查 /dev/md/ 目录下的符号链接来获取。
Added type check for array_path.
"""
if not isinstance(array_path, str): # 添加类型检查
logger.warning(f"传入 _get_actual_md_device_path 的 array_path 不是字符串: {array_path} (类型: {type(array_path)})")
return None
if os.path.exists(array_path):
try:
# os.path.realpath 会解析符号链接,例如 /dev/md/new_raid -> /dev/md127
actual_path = os.path.realpath(array_path)
# 确保解析后仍然是 md 设备(以 /dev/md 开头)
if actual_path.startswith('/dev/md'):
logger.debug(f"已将 RAID 阵列 {array_path} 解析为实际设备路径: {actual_path}")
return actual_path
except Exception as e:
logger.warning(f"无法通过 os.path.realpath 获取 RAID 阵列 {array_path} 的实际设备路径: {e}")
logger.warning(f"RAID 阵列 {array_path} 不存在或无法解析其真实路径。")
return None
def get_device_details_by_path(self, device_path):
"""
根据设备路径获取设备的 UUID 和文件系统类型 (fstype)。
此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。
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): # 添加类型检查
logger.warning(f"传入 get_device_details_by_path 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})")
return None
devices = self.get_block_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}")
if lsblk_details and lsblk_details.get('fstype'): # 即使没有UUID有fstype也算找到部分信息
# 如果直接找到了 fstype就返回 lsblk 提供的 UUID 和 fstype
# 这里的 UUID 应该是文件系统 UUID
logger.debug(f"直接从 lsblk 获取到 {device_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}...")
# 获取 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}")
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}")
if actual_device_lsblk_details and actual_device_lsblk_details.get('fstype'):
# 找到了实际 RAID 设备上的文件系统信息
# 此时的 UUID 是文件系统 UUIDfstype 是文件系统类型
logger.debug(f"在实际设备 {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。
return None
else:
logger.warning(f"无法确定 RAID 阵列 {device_path} 的实际内核设备路径。")
return None # 无法解析实际设备路径,也无法获取 fstype
# 3. 如果仍然没有找到,返回 None
logger.debug(f"未能获取到 {device_path} 的任何详情。")
return None