This commit is contained in:
root
2026-02-10 02:54:13 +08:00
parent 4a59323398
commit 64bfd85368
22 changed files with 2572 additions and 422 deletions

364
system_compat.py Executable file
View File

@@ -0,0 +1,364 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
系统命令兼容性模块
处理不同 Linux 发行版之间系统命令的差异
支持 Arch Linux 和 CentOS/RHEL 8
"""
import subprocess
import json
import logging
import re
import os
from platform_compat import get_platform_compat
logger = logging.getLogger(__name__)
class SystemCommandCompat:
"""系统命令兼容性处理类"""
def __init__(self):
self._lsblk_version = None
self._lvm_version = None
self._parted_version = None
self._compat_cache = {}
def _get_lsblk_version(self):
"""获取 lsblk 版本"""
if self._lsblk_version is None:
try:
# Python 3.6 兼容
result = subprocess.run(
['lsblk', '--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
# 解析版本号,例如 "lsblk from util-linux 2.37.4"
match = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', output)
if match:
self._lsblk_version = tuple(int(x) if x else 0 for x in match.groups())
logger.debug(f"检测到 lsblk 版本: {self._lsblk_version}")
else:
self._lsblk_version = (0, 0, 0)
except Exception as e:
logger.warning(f"无法检测 lsblk 版本: {e}")
self._lsblk_version = (0, 0, 0)
return self._lsblk_version
def _get_lvm_version(self):
"""获取 LVM 版本"""
if self._lvm_version is None:
try:
# Python 3.6 兼容
result = subprocess.run(
['lvm', 'version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
# 解析版本号,例如 "LVM version: 2.03.14"
match = re.search(r'(\d+)\.(\d+)\.(\d+)', output)
if match:
self._lvm_version = tuple(int(x) for x in match.groups())
logger.debug(f"检测到 LVM 版本: {self._lvm_version}")
else:
self._lvm_version = (0, 0, 0)
except Exception as e:
logger.warning(f"无法检测 LVM 版本: {e}")
self._lvm_version = (0, 0, 0)
return self._lvm_version
def supports_lsblk_json(self):
"""检查 lsblk 是否支持 JSON 输出 (util-linux 2.23+)"""
version = self._get_lsblk_version()
# util-linux 2.23 引入 JSON 支持
return version >= (2, 23)
def supports_lvm_json(self):
"""检查 LVM 是否支持 JSON 报告格式 (2.02.166+)"""
version = self._get_lvm_version()
# LVM 2.02.166 引入 JSON 报告格式
return version >= (2, 2, 166) or version >= (2, 3, 0)
def get_lsblk_command(self):
"""
获取适用于当前系统的 lsblk 命令
返回: (命令列表, 是否需要手动解析)
"""
# 注意: PATH 列在较新的 lsblk 中可用,但在 CentOS 8 等旧系统中不可用
# 使用 KNAME 代替,它是内部内核设备名称,总是存在
if self.supports_lsblk_json():
# 现代系统支持 JSON
return [
"lsblk", "-J", "-o",
"NAME,KNAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME"
], False
else:
# 旧系统回退到普通输出
logger.warning("lsblk 不支持 JSON 输出,使用回退方案")
return [
"lsblk", "-o",
"NAME,KNAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME",
"-n", "-p" # -n 不打印标题,-p 显示完整路径
], True
def parse_lsblk_output(self, stdout, use_manual_parse=False):
"""
解析 lsblk 输出
:param stdout: 命令输出
:param use_manual_parse: 是否需要手动解析(非 JSON
:return: 设备列表
"""
if not use_manual_parse:
try:
data = json.loads(stdout)
devices = data.get('blockdevices', [])
# 统一处理 PATH 字段
# 注意: 使用 kname (内核设备名) 来生成 path因为 PATH 列在某些系统中不可用
def normalize_device(dev):
# 优先使用 kname (内核设备名),因为它总是存在且是规范的
if 'kname' in dev:
dev['path'] = f"/dev/{dev['kname']}"
elif 'path' not in dev and 'name' in dev:
dev['path'] = f"/dev/{dev['name']}"
# 清理旧字段
if 'PATH' in dev:
dev.pop('PATH')
if 'KNAME' in dev:
dev['kname'] = dev.pop('KNAME')
if 'children' in dev:
for child in dev['children']:
normalize_device(child)
return dev
for dev in devices:
normalize_device(dev)
return devices
except json.JSONDecodeError as e:
logger.error(f"JSON 解析失败,尝试手动解析: {e}")
use_manual_parse = True
if use_manual_parse:
return self._manual_parse_lsblk(stdout)
return []
def _manual_parse_lsblk(self, stdout):
"""
手动解析 lsblk 的普通输出
用于不支持 JSON 的旧系统
"""
devices = []
device_stack = [] # 用于处理层次结构
lines = stdout.strip().split('\n')
for line in lines:
if not line.strip():
continue
# 解析缩进级别(每两个空格或一个制表符表示一个层级)
indent_level = 0
stripped_line = line
while stripped_line.startswith(' ') or stripped_line.startswith('') or stripped_line.startswith('') or stripped_line.startswith(''):
if stripped_line[0] in '├└│':
indent_level += 1
stripped_line = stripped_line[1:]
if stripped_line.startswith(''):
stripped_line = stripped_line[1:]
if stripped_line.startswith(' '):
stripped_line = stripped_line[1:]
# 分割字段
parts = stripped_line.split()
if len(parts) < 6:
continue
device = {
'name': parts[0],
'path': f"/dev/{parts[0]}" if not parts[0].startswith('/') else parts[0],
'fstype': parts[1] if parts[1] != '' else None,
'size': parts[2],
'mountpoint': parts[3] if parts[3] != '' else None,
'ro': parts[4] == '1',
'type': parts[5],
}
# 添加可选字段
if len(parts) > 6:
device['uuid'] = parts[6] if parts[6] != '' else None
if len(parts) > 7:
device['partuuid'] = parts[7] if parts[7] != '' else None
# 处理层次结构
if indent_level == 0:
devices.append(device)
device_stack = [device]
else:
# 找到正确的父设备
while len(device_stack) > indent_level:
device_stack.pop()
if device_stack:
parent = device_stack[-1]
if 'children' not in parent:
parent['children'] = []
parent['children'].append(device)
device_stack.append(device)
else:
devices.append(device)
device_stack = [device]
return devices
def get_lvm_commands(self):
"""
获取适用于当前系统的 LVM 命令
返回: {
'pvs': (命令列表, 是否需要手动解析),
'vgs': (命令列表, 是否需要手动解析),
'lvs': (命令列表, 是否需要手动解析)
}
"""
if self.supports_lvm_json():
return {
'pvs': (['pvs', '--reportformat', 'json'], False),
'vgs': (['vgs', '--reportformat', 'json'], False),
'lvs': (['lvs', '-o', 'lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path', '--reportformat', 'json'], False),
}
else:
logger.warning("LVM 不支持 JSON 输出,使用回退方案")
return {
'pvs': (['pvs', '--noheadings', '--separator', '|', '-o', 'pv_name,vg_name,pv_uuid,pv_size,pv_free,pv_attr,pv_fmt'], True),
'vgs': (['vgs', '--noheadings', '--separator', '|', '-o', 'vg_name,vg_uuid,vg_size,vg_free,vg_attr,pv_count,lv_count,vg_alloc_percent,vg_fmt'], True),
'lvs': (['lvs', '--noheadings', '--separator', '|', '-o', 'lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path'], True),
}
def parse_pvs_output(self, stdout, use_manual_parse=False):
"""解析 pvs 输出"""
if not use_manual_parse:
try:
data = json.loads(stdout)
if 'report' in data and data['report']:
return [{
'pv_name': pv.get('pv_name'),
'vg_name': pv.get('vg_name'),
'pv_uuid': pv.get('pv_uuid'),
'pv_size': pv.get('pv_size'),
'pv_free': pv.get('pv_free'),
'pv_attr': pv.get('pv_attr'),
'pv_fmt': pv.get('pv_fmt')
} for pv in data['report'][0].get('pv', [])]
return []
except json.JSONDecodeError:
use_manual_parse = True
if use_manual_parse:
pvs = []
for line in stdout.strip().split('\n'):
parts = line.strip().split('|')
if len(parts) >= 7:
pvs.append({
'pv_name': parts[0].strip(),
'vg_name': parts[1].strip() if parts[1].strip() else None,
'pv_uuid': parts[2].strip() if len(parts) > 2 else None,
'pv_size': parts[3].strip() if len(parts) > 3 else None,
'pv_free': parts[4].strip() if len(parts) > 4 else None,
'pv_attr': parts[5].strip() if len(parts) > 5 else None,
'pv_fmt': parts[6].strip() if len(parts) > 6 else None,
})
return pvs
return []
def parse_vgs_output(self, stdout, use_manual_parse=False):
"""解析 vgs 输出"""
if not use_manual_parse:
try:
data = json.loads(stdout)
if 'report' in data and data['report']:
return [{
'vg_name': vg.get('vg_name'),
'vg_uuid': vg.get('vg_uuid'),
'vg_size': vg.get('vg_size'),
'vg_free': vg.get('vg_free'),
'vg_attr': vg.get('vg_attr'),
'pv_count': vg.get('pv_count'),
'lv_count': vg.get('lv_count'),
'vg_alloc_percent': vg.get('vg_alloc_percent'),
'vg_fmt': vg.get('vg_fmt')
} for vg in data['report'][0].get('vg', [])]
return []
except json.JSONDecodeError:
use_manual_parse = True
if use_manual_parse:
vgs = []
for line in stdout.strip().split('\n'):
parts = line.strip().split('|')
if len(parts) >= 9:
vgs.append({
'vg_name': parts[0].strip(),
'vg_uuid': parts[1].strip() if len(parts) > 1 else None,
'vg_size': parts[2].strip() if len(parts) > 2 else None,
'vg_free': parts[3].strip() if len(parts) > 3 else None,
'vg_attr': parts[4].strip() if len(parts) > 4 else None,
'pv_count': parts[5].strip() if len(parts) > 5 else None,
'lv_count': parts[6].strip() if len(parts) > 6 else None,
'vg_alloc_percent': parts[7].strip() if len(parts) > 7 else None,
'vg_fmt': parts[8].strip() if len(parts) > 8 else None,
})
return vgs
return []
def parse_lvs_output(self, stdout, use_manual_parse=False):
"""解析 lvs 输出"""
if not use_manual_parse:
try:
data = json.loads(stdout)
if 'report' in data and data['report']:
return [{
'lv_name': lv.get('lv_name'),
'vg_name': lv.get('vg_name'),
'lv_uuid': lv.get('lv_uuid'),
'lv_size': lv.get('lv_size'),
'lv_attr': lv.get('lv_attr'),
'origin': lv.get('origin'),
'snap_percent': lv.get('snap_percent'),
'lv_path': lv.get('lv_path')
} for lv in data['report'][0].get('lv', [])]
return []
except json.JSONDecodeError:
use_manual_parse = True
if use_manual_parse:
lvs = []
for line in stdout.strip().split('\n'):
parts = line.strip().split('|')
if len(parts) >= 8:
lvs.append({
'lv_name': parts[0].strip(),
'vg_name': parts[1].strip(),
'lv_uuid': parts[2].strip() if len(parts) > 2 else None,
'lv_size': parts[3].strip() if len(parts) > 3 else None,
'lv_attr': parts[4].strip() if len(parts) > 4 else None,
'origin': parts[5].strip() if len(parts) > 5 else None,
'snap_percent': parts[6].strip() if len(parts) > 6 else None,
'lv_path': parts[7].strip() if len(parts) > 7 else None,
})
return lvs
return []
# 全局实例
_compat_instance = None
def get_system_compat():
"""获取全局系统兼容性实例"""
global _compat_instance
if _compat_instance is None:
_compat_instance = SystemCommandCompat()
return _compat_instance