Files
diskmanager2/system_compat.py
2026-02-10 02:54:13 +08:00

365 lines
15 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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