fix bug
This commit is contained in:
364
system_compat.py
Executable file
364
system_compat.py
Executable 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
|
||||
Reference in New Issue
Block a user