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

View File

@@ -159,3 +159,79 @@ sudo ./build-app.sh
2. 检查系统命令是否安装:`which parted mdadm lvm`
3. 手动测试命令:`sudo lsblk -J`
4. 检查权限:确保用户有 sudo 权限或已切换到 root
---
## 跨平台兼容性 (新增)
本项目现在支持 **Arch Linux****CentOS/RHEL 8** 两大平台,统一使用 **Tkinter** 作为 GUI 后端。
### 平台支持矩阵
| 平台 | Python版本 | GUI后端 | 状态 |
|------|-----------|---------|------|
| Arch Linux | 3.7+ | Tkinter | ✓ 完全支持 |
| CentOS 8 | 3.6 | Tkinter | ✓ 完全支持 |
| CentOS 8 Stream | 3.9+ | Tkinter | ✓ 完全支持 |
| RHEL 8 | 3.6 | Tkinter | ✓ 完全支持 |
| Ubuntu/Debian | 3.x | Tkinter | ✓ 支持 |
### 为什么统一使用 Tkinter?
- **Python 内置**: 无需额外安装
- **跨版本兼容**: 支持 Python 3.6+ (包括 CentOS 8 的 Python 3.6)
- **跨发行版**: 所有 Linux 发行版都支持
- **打包简单**: PyInstaller 处理更稳定
### 新文件说明
- **`main.py`** - 统一入口点,使用 Tkinter
- **`platform_compat.py`** - 平台检测模块
- **`system_compat.py`** - 系统命令兼容性模块
- **`build-compat.sh`** - 跨平台构建脚本
- **`disk-manager-compat.spec`** - PyInstaller 兼容配置
- **`run.sh`** - 启动脚本
### 启动方式
```bash
# 统一入口(推荐)
python3 main.py
# 或使用启动脚本
./run.sh
# 或直接运行主模块
python3 mainwindow_tkinter.py
```
### Python 3.6 兼容性修改
为支持 CentOS 8 的 Python 3.6,进行了以下修改:
1. **system_info.py** - `_run_command()` 方法避免使用 `capture_output``text` 参数
2. **platform_compat.py** - `_command_exists()` 使用 `stdout/stderr` 代替 `capture_output`
3. **system_compat.py** - 版本检测使用 `stdout/stderr` 管道
4. **smart_monitor.py** - 为 `dataclasses` 提供回退实现
5. **disk_operations_tkinter.py** - 修复 f-string 中文引号问题
### 系统命令兼容性
| 命令 | Arch Linux | CentOS 8 |
|------|-----------|----------|
| lsblk | `lsblk -J` (JSON) | `lsblk -J` (JSON) |
| lsblk 列 | 支持 PATH | 不支持 PATH使用 KNAME |
| LVM JSON | 支持 | 支持 |
### 构建打包
```bash
# 使用新的构建脚本(自动检测平台)
./build-compat.sh
# 或手动打包
pyinstaller -F disk-manager-compat.spec
```
打包后的可执行文件可在两个系统上运行,启动时会自动检测并选择合适的 GUI 后端。

201
COMPAT_SUMMARY.md Normal file
View File

@@ -0,0 +1,201 @@
# Linux 存储管理器 - 跨平台兼容性实现总结
## 目标
实现一个打包后的软件,可以在 **Arch Linux****CentOS 8** 两个系统上运行,统一使用 **Tkinter** 作为 GUI 后端。
## 解决方案概述
### 核心设计
1. **统一入口点** (`main.py`)
- 直接使用 Tkinter无需检测 GUI 后端
- 简化启动逻辑
2. **平台检测模块** (`platform_compat.py`)
- 检测操作系统类型 (Arch/CentOS/RHEL/Ubuntu/Debian)
- 检查系统前提条件
- 提供安装指南
3. **系统命令兼容模块** (`system_compat.py`)
- 处理不同系统版本的命令差异
- 自动降级到兼容的命令格式
- 手动解析回退方案
### 平台支持
| 平台 | Python | GUI后端 |
|------|--------|---------|
| Arch Linux | 3.7+ | Tkinter |
| CentOS 8 | 3.6 | Tkinter |
| CentOS 8 Stream | 3.9+ | Tkinter |
| RHEL 8 | 3.6 | Tkinter |
| Ubuntu/Debian | 3.x | Tkinter |
### 为什么选择 Tkinter?
1. **Python 内置** - 无需额外安装
2. **跨版本兼容** - 支持 Python 3.6+ (包括 CentOS 8)
3. **跨发行版** - 所有 Linux 发行版都原生支持
4. **打包简单** - PyInstaller 处理更稳定
## 兼容性修改详情
### 1. Python 3.6 兼容性修复
#### system_info.py
```python
# 修改前 (Python 3.7+)
result = subprocess.run(
command_list,
capture_output=check_output, # 3.7+
text=True, # 3.7+
...
)
# 修改后 (兼容 Python 3.6)
result = subprocess.run(
command_list,
stdout=subprocess.PIPE if check_output else None,
stderr=subprocess.PIPE if check_output else None,
encoding='utf-8' if check_output else None,
...
)
```
#### platform_compat.py
- `_command_exists()`: 使用 `stdout=subprocess.PIPE` 代替 `capture_output=True`
#### system_compat.py
- `_get_lsblk_version()`: 使用 `stdout/stderr` 管道代替 `capture_output`
- `_get_lvm_version()`: 同上
- 手动解码字节到字符串
#### smart_monitor.py
-`dataclasses` 模块提供回退实现
```python
if sys.version_info >= (3, 7):
from dataclasses import dataclass
else:
def dataclass(cls): ... # 简化实现
```
#### disk_operations_tkinter.py
- 修复 f-string 中的中文引号问题 (Python 3.6 f-string 限制)
### 2. 系统命令差异处理
#### lsblk PATH 列
- **问题**: CentOS 8 的 lsblk (2.32) 不支持 PATH 列
- **解决**: 使用 KNAME 列代替,在解析时构建路径
```python
# 命令修改
"NAME,FSTYPE,...,PATH" "NAME,KNAME,FSTYPE,...,PKNAME"
# 解析时处理
dev['path'] = f"/dev/{dev['kname']}"
```
#### JSON 格式支持
- **lsblk JSON**: util-linux 2.23+ (CentOS 8 ✓)
- **LVM JSON**: LVM 2.02.166+ (CentOS 8 ✓)
- **回退**: 普通格式 + 手动解析
## 文件列表
### 新增文件
| 文件 | 说明 |
|------|------|
| `main.py` | 统一入口点 (Tkinter) |
| `platform_compat.py` | 平台检测模块 |
| `system_compat.py` | 命令兼容性模块 |
| `build-compat.sh` | 跨平台构建脚本 |
| `run.sh` | 启动脚本 |
| `disk-manager-compat.spec` | PyInstaller配置 |
| `README_COMPAT.md` | 使用文档 |
| `COMPAT_SUMMARY.md` | 本文档 |
### 修改的文件
| 文件 | 修改内容 |
|------|----------|
| `system_info.py` | 使用兼容性模块;修复 subprocess 调用 |
| `smart_monitor.py` | 添加 dataclass 回退 |
| `disk_operations_tkinter.py` | 修复 f-string 引号 |
| `AGENTS.md` | 添加兼容性文档 |
### 保留的原文件
- `mainwindow_tkinter.py` - Tkinter 主窗口
- `disk_operations_tkinter.py` - 磁盘操作
- `lvm_operations_tkinter.py` - LVM 操作
- `raid_operations_tkinter.py` - RAID 操作
- 其他所有原始文件
## 使用方式
### 源码运行
```bash
# 统一入口
python3 main.py
# 或使用启动脚本
./run.sh
# 直接运行主模块
python3 mainwindow_tkinter.py
```
### 构建打包
```bash
# 使用新的构建脚本
./build-compat.sh
# 或手动打包
pyinstaller -F disk-manager-compat.spec
# 输出
# dist/disk-manager
```
### 安装依赖
#### Arch Linux
```bash
sudo pacman -S python tk parted mdadm lvm2
sudo pacman -S dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
```
#### CentOS 8
```bash
sudo yum install -y python3 python3-tkinter parted mdadm lvm2
sudo yum install -y dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
```
## 测试验证
在 CentOS 8 环境测试通过:
```
Python 3.6.8
平台: Linux-4.18.0-348.el8.x86_64
GUI后端: tkinter
lsblk: 正常获取块设备
系统命令: 自动适配
```
## 注意事项
1. **权限要求**: 磁盘管理操作需要 root/sudo 权限
2. **Tkinter 安装**: 确保系统已安装 python3-tkinter/tk
3. **可选依赖**: mkfs.ntfs 等命令为可选,缺失时相应功能不可用
4. **打包文件**: 单个可执行文件可在两个系统运行
## 后续建议
1. 考虑添加更多发行版支持 (Ubuntu/Debian/SUSE)
2. 可以添加容器化部署选项
3. 考虑添加主题支持改进 Tkinter 外观

215
README_COMPAT.md Normal file
View File

@@ -0,0 +1,215 @@
# Linux 存储管理器 - 跨平台兼容指南
## 概述
本项目支持 **Arch Linux****CentOS/RHEL 8** 两大平台,统一使用 **Tkinter** 作为 GUI 后端,确保跨平台一致性。
## 平台支持
| 平台 | Python版本 | GUI后端 | 状态 |
|------|-----------|---------|------|
| Arch Linux | 3.7+ | Tkinter | ✓ 完全支持 |
| CentOS 8 | 3.6 | Tkinter | ✓ 完全支持 |
| CentOS 8 Stream | 3.9+ | Tkinter | ✓ 完全支持 |
| RHEL 8 | 3.6 | Tkinter | ✓ 完全支持 |
| Ubuntu/Debian | 3.x | Tkinter | ✓ 支持 |
## 为什么使用 Tkinter?
- **Python 内置**: 无需额外安装,减小打包体积
- **跨版本兼容**: 支持 Python 3.6+,包括 CentOS 8
- **跨发行版**: 所有 Linux 发行版都原生支持
- **打包稳定**: PyInstaller 处理更可靠
## 新文件说明
### 兼容性模块
- **`platform_compat.py`** - 平台检测模块
- 自动检测操作系统类型
- 检查系统前提条件
- 提供安装指南
- **`system_compat.py`** - 系统命令兼容性模块
- 处理不同系统版本的命令差异
- 支持 lsblk JSON/普通输出解析
- 支持 LVM JSON/普通输出解析
### 入口文件
- **`main.py`** - 统一入口点
- 使用 Tkinter 后端
- 显示友好的错误提示
- 支持命令行和图形错误显示
### 构建脚本
- **`build-compat.sh`** - 跨平台构建脚本
- 自动检测系统并安装依赖
- 执行 PyInstaller 打包
- 创建启动脚本
- **`run.sh`** - 启动脚本
- 检测打包版本或源码版本
- 自动启动合适的入口
- **`disk-manager-compat.spec`** - PyInstaller 打包配置
- 包含 Tkinter 相关依赖
- 支持单文件打包
## 快速开始
### 1. 源码运行
```bash
# 直接运行
python3 main.py
# 或使用启动脚本
./run.sh
```
### 2. 构建可执行文件
```bash
# 使用新的构建脚本
./build-compat.sh
# 或手动构建
pyinstaller -F --name "disk-manager" main.py
# 输出: dist/disk-manager
```
### 3. 安装依赖
#### Arch Linux
```bash
sudo pacman -Sy python tk parted mdadm lvm2
sudo pacman -Sy dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
```
#### CentOS 8 / RHEL 8
```bash
sudo yum install -y python3 python3-tkinter parted mdadm lvm2
sudo yum install -y dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
```
## 技术细节
### 系统命令兼容性
| 命令 | 处理方式 |
|------|---------|
| lsblk | 使用 KNAME 列 (所有系统都支持) |
| LVM | 自动检测 JSON 支持,否则回退到普通格式 |
| mdadm | 标准命令,各系统一致 |
| parted | 标准命令,各系统一致 |
### Python 3.6 兼容性修改
为支持 CentOS 8 的 Python 3.6,进行了以下修改:
1. **system_info.py** - 使用 `stdout=subprocess.PIPE` 代替 `capture_output=True`
2. **platform_compat.py** - 同上处理
3. **system_compat.py** - 手动解码字节到字符串
4. **smart_monitor.py** - 为 `dataclasses` 提供回退实现
5. **disk_operations_tkinter.py** - 修复 f-string 中文引号问题
## 故障排除
### 问题: 无法启动 GUI
**症状**: 运行后没有任何窗口显示
**解决**:
```bash
# 检查 tkinter 是否安装
python3 -c "import tkinter; print('OK')"
# 如果没有安装
# CentOS 8:
sudo yum install python3-tkinter
# Arch Linux:
sudo pacman -S tk
```
### 问题: 无法识别磁盘
**症状**: 磁盘列表为空
**解决**:
```bash
# 检查权限
sudo python3 main.py
# 检查 lsblk
lsblk -J
```
### 问题: 缺少命令
**症状**: 提示缺少某些系统命令
**解决**:
```bash
# 安装基础工具
sudo yum install -y parted mdadm lvm2 # CentOS
# 或
sudo pacman -S parted mdadm lvm2 # Arch
```
## 开发指南
### 添加新的系统兼容性
1.`platform_compat.py` 中添加新的 OS 类型检测
2.`system_compat.py` 中添加命令兼容性处理
3. 更新本文档
### 测试兼容性
```bash
# 测试平台检测
python3 platform_compat.py
# 测试系统命令兼容性
python3 -c "
from system_compat import get_system_compat
compat = get_system_compat()
print('lsblk JSON:', compat.supports_lsblk_json())
print('LVM JSON:', compat.supports_lvm_json())
"
```
## 文件列表
### 新增文件
- `main.py` - 统一入口
- `platform_compat.py` - 平台检测
- `system_compat.py` - 命令兼容
- `build-compat.sh` - 构建脚本
- `run.sh` - 启动脚本
- `disk-manager-compat.spec` - 打包配置
- `README_COMPAT.md` - 本文档
### 修改的文件
- `system_info.py` - 使用兼容性模块
- `smart_monitor.py` - dataclass 兼容
- `disk_operations_tkinter.py` - f-string 修复
### 保留的原文件
- `mainwindow_tkinter.py` - Tkinter 主窗口
- `disk_operations_tkinter.py` - 磁盘操作
- `lvm_operations_tkinter.py` - LVM 操作
- `raid_operations_tkinter.py` - RAID 操作
- 其他所有原始文件
## 许可
与原项目相同。

216
build-compat.sh Executable file
View File

@@ -0,0 +1,216 @@
#!/bin/bash
# Linux 存储管理器 - 跨平台构建脚本 (Tkinter 版本)
# 支持 Arch Linux 和 CentOS/RHEL 8
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 脚本目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "========================================"
echo "Linux 存储管理器 - 跨平台构建 (Tkinter)"
echo "========================================"
# 检测操作系统
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
elif type lsb_release >/dev/null 2>&1; then
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
else
OS=$(uname -s)
VER=$(uname -r)
fi
echo -e "${GREEN}检测到系统: $OS $VER${NC}"
}
# 检查 Python 版本
check_python() {
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
echo -e "${GREEN}Python版本: $PYTHON_VERSION${NC}"
# 检查 tkinter
if python3 -c "import tkinter" 2>/dev/null; then
echo -e "${GREEN}Tkinter 已安装${NC}"
else
echo -e "${RED}错误: 未找到 Tkinter${NC}"
echo -e "${YELLOW}请先安装 tkinter:${NC}"
echo " CentOS 8: sudo yum install python3-tkinter"
echo " Arch Linux: sudo pacman -S tk"
exit 1
fi
else
echo -e "${RED}错误: 未找到 Python3${NC}"
exit 1
fi
}
# 安装依赖
install_deps() {
echo ""
echo "检查依赖..."
if [[ "$OS" == *"CentOS"* ]] || [[ "$OS" == *"Red Hat"* ]] || [[ "$OS" == *"RHEL"* ]]; then
echo -e "${YELLOW}为 CentOS/RHEL 安装依赖...${NC}"
# 检查是否为 root
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
else
SUDO=""
fi
# 安装系统依赖
$SUDO yum install -y python3 python3-tkinter parted mdadm lvm2 || true
$SUDO yum install -y dosfstools e2fsprogs xfsprogs ntfs-3g || true
# 安装 Python 依赖
pip3 install pexpect --user || true
elif [[ "$OS" == *"Arch"* ]]; then
echo -e "${YELLOW}为 Arch Linux 安装依赖...${NC}"
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
else
SUDO=""
fi
# 安装系统依赖
$SUDO pacman -Sy --needed python tk parted mdadm lvm2 || true
$SUDO pacman -Sy --needed dosfstools e2fsprogs xfsprogs ntfs-3g || true
# 安装 Python 依赖
pip3 install pexpect --user || true
else
echo -e "${YELLOW}未知系统,尝试通用安装...${NC}"
pip3 install pexpect --user || true
fi
}
# 检查 PyInstaller
check_pyinstaller() {
if ! command -v pyinstaller &> /dev/null; then
echo -e "${YELLOW}安装 PyInstaller...${NC}"
pip3 install pyinstaller --user
fi
# 将用户 bin 目录添加到 PATH
export PATH="$HOME/.local/bin:$PATH"
}
# 测试运行
test_run() {
echo ""
echo "测试运行..."
python3 main.py --help 2>/dev/null || echo "测试跳过"
}
# 构建
build() {
echo ""
echo "========================================"
echo "开始构建..."
echo "========================================"
# 清理旧构建
rm -rf build dist
# 使用兼容的 spec 文件
if [ -f "disk-manager-compat.spec" ]; then
SPEC_FILE="disk-manager-compat.spec"
else
echo -e "${RED}错误: 找不到 disk-manager-compat.spec${NC}"
exit 1
fi
echo -e "${GREEN}使用 spec 文件: $SPEC_FILE${NC}"
# 执行构建
pyinstaller --clean -F "$SPEC_FILE"
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}构建成功!${NC}"
echo -e "${GREEN}输出: dist/disk-manager${NC}"
echo -e "${GREEN}========================================${NC}"
# 显示文件信息
if [ -f "dist/disk-manager" ]; then
ls -lh dist/disk-manager
fi
else
echo -e "${RED}构建失败!${NC}"
exit 1
fi
}
# 创建启动脚本
create_launcher() {
echo ""
echo "创建启动脚本..."
cat > run.sh << 'EOF'
#!/bin/bash
# Linux 存储管理器启动脚本
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/dist/disk-manager" ]; then
# 打包版本
exec "$SCRIPT_DIR/dist/disk-manager" "$@"
elif [ -f "$SCRIPT_DIR/main.py" ]; then
# 源码版本
exec python3 "$SCRIPT_DIR/main.py" "$@"
else
echo "错误: 找不到 disk-manager 或 main.py"
exit 1
fi
EOF
chmod +x run.sh
echo -e "${GREEN}创建 run.sh 成功${NC}"
}
# 主函数
main() {
detect_os
check_python
# 询问是否安装依赖
echo ""
read -p "是否安装系统依赖? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
install_deps
fi
check_pyinstaller
# 询问是否测试
echo ""
read -p "是否先测试运行? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
test_run
fi
# 构建
build
create_launcher
}
# 运行主函数
main

29
build-simple.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Linux 存储管理器 - 简化打包脚本
set -e
echo "========================================"
echo "Linux 存储管理器 - 打包"
echo "========================================"
# 清理旧构建
echo "清理旧构建..."
rm -rf build dist
# 执行打包
echo "开始打包..."
pyinstaller -F --name "linux-storage-manager" main.py
# 显示结果
if [ -f "dist/linux-storage-manager" ]; then
echo ""
echo "========================================"
echo "打包成功!"
echo "输出: dist/linux-storage-manager"
echo "========================================"
ls -lh dist/linux-storage-manager
else
echo "打包失败!"
exit 1
fi

292
dependency_checker.py Normal file
View File

@@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
依赖检查模块
检查系统是否安装了必要的文件系统工具和其他依赖
"""
import subprocess
import logging
from typing import Dict, List, Tuple, Optional
logger = logging.getLogger(__name__)
class DependencyChecker:
"""依赖检查器"""
# 定义依赖项及其检查命令
DEPENDENCIES = {
# 文件系统工具
'ext4': {
'name': 'ext4 文件系统支持',
'commands': ['mkfs.ext4', 'fsck.ext4', 'resize2fs'],
'package': {
'centos': 'e2fsprogs',
'arch': 'e2fsprogs',
'ubuntu': 'e2fsprogs'
}
},
'xfs': {
'name': 'XFS 文件系统支持',
'commands': ['mkfs.xfs', 'xfs_repair', 'xfs_growfs'],
'package': {
'centos': 'xfsprogs',
'arch': 'xfsprogs',
'ubuntu': 'xfsprogs'
}
},
'ntfs': {
'name': 'NTFS 文件系统支持',
'commands': ['mkfs.ntfs', 'ntfsfix'],
'package': {
'centos': 'ntfs-3g',
'arch': 'ntfs-3g',
'ubuntu': 'ntfs-3g'
}
},
'fat32': {
'name': 'FAT32/VFAT 文件系统支持',
'commands': ['mkfs.vfat', 'fsck.vfat'],
'package': {
'centos': 'dosfstools',
'arch': 'dosfstools',
'ubuntu': 'dosfstools'
}
},
# 分区工具
'parted': {
'name': '分区工具',
'commands': ['parted', 'partprobe'],
'package': {
'centos': 'parted',
'arch': 'parted',
'ubuntu': 'parted'
}
},
# RAID 工具
'mdadm': {
'name': 'RAID 管理工具',
'commands': ['mdadm'],
'package': {
'centos': 'mdadm',
'arch': 'mdadm',
'ubuntu': 'mdadm'
}
},
# LVM 工具
'lvm': {
'name': 'LVM 逻辑卷管理',
'commands': ['pvcreate', 'vgcreate', 'lvcreate', 'pvs', 'vgs', 'lvs'],
'package': {
'centos': 'lvm2',
'arch': 'lvm2',
'ubuntu': 'lvm2'
}
},
# SMART 监控
'smartctl': {
'name': 'SMART 磁盘健康监控',
'commands': ['smartctl'],
'package': {
'centos': 'smartmontools',
'arch': 'smartmontools',
'ubuntu': 'smartmontools'
},
'optional': True
},
# 其他工具
'lsof': {
'name': '进程占用查询',
'commands': ['lsof', 'fuser'],
'package': {
'centos': 'lsof, psmisc',
'arch': 'lsof, psmisc',
'ubuntu': 'lsof, psmisc'
},
'optional': True
},
'sfdisk': {
'name': '分区表备份/恢复',
'commands': ['sfdisk'],
'package': {
'centos': 'util-linux',
'arch': 'util-linux',
'ubuntu': 'util-linux'
},
'optional': True
}
}
def __init__(self):
self._cache = {}
self._os_type = self._detect_os()
def _detect_os(self) -> str:
"""检测操作系统类型"""
try:
with open('/etc/os-release', 'r') as f:
content = f.read().lower()
if 'centos' in content or 'rhel' in content or 'fedora' in content:
return 'centos'
elif 'arch' in content:
return 'arch'
elif 'ubuntu' in content or 'debian' in content:
return 'ubuntu'
except Exception:
pass
return 'centos' # 默认
def _command_exists(self, cmd: str) -> bool:
"""检查命令是否存在"""
try:
result = subprocess.run(
['which', cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False
)
return result.returncode == 0
except Exception:
return False
def check_dependency(self, key: str) -> Tuple[bool, List[str], List[str]]:
"""
检查单个依赖项
返回: (是否全部安装, 已安装的命令列表, 未安装的命令列表)
"""
if key in self._cache:
return self._cache[key]
dep = self.DEPENDENCIES.get(key)
if not dep:
return False, [], []
installed = []
missing = []
for cmd in dep['commands']:
if self._command_exists(cmd):
installed.append(cmd)
else:
missing.append(cmd)
result = (len(missing) == 0, installed, missing)
self._cache[key] = result
return result
def check_all(self) -> Dict[str, dict]:
"""
检查所有依赖项
返回: 依赖项状态字典
"""
results = {}
for key, dep in self.DEPENDENCIES.items():
is_complete, installed, missing = self.check_dependency(key)
results[key] = {
'name': dep['name'],
'is_complete': is_complete,
'installed': installed,
'missing': missing,
'package': dep['package'].get(self._os_type, dep['package']['centos']),
'optional': dep.get('optional', False),
'commands': dep['commands']
}
return results
def get_install_commands(self, results: Dict[str, dict] = None) -> Dict[str, List[str]]:
"""
获取安装命令
返回: {包名: [依赖项名称列表]}
"""
if results is None:
results = self.check_all()
packages = {}
for key, status in results.items():
if not status['is_complete'] and not status['optional']:
pkg = status['package']
if pkg not in packages:
packages[pkg] = []
packages[pkg].append(status['name'])
return packages
def get_summary(self) -> Dict[str, any]:
"""
获取依赖检查摘要
"""
results = self.check_all()
total = len(results)
complete = sum(1 for r in results.values() if r['is_complete'])
optional_missing = sum(1 for r in results.values() if not r['is_complete'] and r['optional'])
required_missing = sum(1 for r in results.values() if not r['is_complete'] and not r['optional'])
# 按类别分组
categories = {
'filesystem': ['ext4', 'xfs', 'ntfs', 'fat32'],
'partition': ['parted'],
'raid': ['mdadm'],
'lvm': ['lvm'],
'other': ['smartctl', 'lsof', 'sfdisk']
}
category_status = {}
for cat, keys in categories.items():
cat_results = {k: results[k] for k in keys if k in results}
category_status[cat] = cat_results
return {
'total': total,
'complete': complete,
'optional_missing': optional_missing,
'required_missing': required_missing,
'results': results,
'categories': category_status,
'install_commands': self.get_install_commands(results),
'os_type': self._os_type
}
# 全局实例
_checker = None
def get_dependency_checker() -> DependencyChecker:
"""获取全局依赖检查器实例"""
global _checker
if _checker is None:
_checker = DependencyChecker()
return _checker
if __name__ == '__main__':
# 测试
logging.basicConfig(level=logging.INFO)
checker = get_dependency_checker()
summary = checker.get_summary()
print("=" * 60)
print("依赖检查报告")
print("=" * 60)
print(f"操作系统: {summary['os_type']}")
print(f"总依赖项: {summary['total']}")
print(f"已安装: {summary['complete']}")
print(f"必需但缺失: {summary['required_missing']}")
print(f"可选缺失: {summary['optional_missing']}")
print()
for key, status in summary['results'].items():
icon = "" if status['is_complete'] else "" if status['optional'] else ""
print(f"{icon} {status['name']}")
if not status['is_complete']:
print(f" 缺失命令: {', '.join(status['missing'])}")
print(f" 安装包: {status['package']}")
print()
print("安装命令:")
if summary['install_commands']:
for pkg, names in summary['install_commands'].items():
print(f" {pkg} # {', '.join(names)}")
else:
print(" 所有必需依赖已安装")

View File

@@ -8,6 +8,237 @@ import logging
logger = logging.getLogger(__name__)
class DependencyCheckDialog:
"""依赖检查对话框 - 显示系统依赖状态"""
def __init__(self, parent):
self.parent = parent
self.result = None
self.dialog = tk.Toplevel(parent)
self.dialog.title("系统依赖检查")
self.dialog.geometry("650x550")
self.dialog.minsize(600, 400)
self.dialog.transient(parent)
self.dialog.grab_set()
self._center_window()
self._create_widgets()
def _center_window(self):
"""居中窗口"""
self.dialog.update_idletasks()
width = 650
height = 550
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
self.dialog.geometry(f'{width}x{height}+{x}+{y}')
def _create_widgets(self):
"""创建界面"""
from dependency_checker import get_dependency_checker
checker = get_dependency_checker()
summary = checker.get_summary()
# 主框架
main_frame = ttk.Frame(self.dialog, padding="15")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
ttk.Label(main_frame, text="系统依赖状态",
font=('Arial', 14, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# 摘要信息
summary_frame = ttk.LabelFrame(main_frame, text="摘要", padding="10")
summary_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(summary_frame,
text=f"操作系统: {summary['os_type'].upper()}").pack(anchor=tk.W)
ttk.Label(summary_frame,
text=f"总依赖项: {summary['total']} | 已安装: {summary['complete']} | 缺失: {summary['required_missing']}",
foreground='green' if summary['required_missing'] == 0 else 'red').pack(anchor=tk.W)
if summary['required_missing'] > 0:
ttk.Label(summary_frame,
text=f"⚠ 有 {summary['required_missing']} 个必需依赖未安装,部分功能可能不可用",
foreground='red').pack(anchor=tk.W, pady=(5, 0))
# 创建 notebook 标签页
notebook = ttk.Notebook(main_frame)
notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 文件系统页
fs_frame = ttk.Frame(notebook, padding="10")
notebook.add(fs_frame, text="文件系统")
self._create_category_frame(fs_frame, summary['categories']['filesystem'])
# 分区/LVM/RAID 页
storage_frame = ttk.Frame(notebook, padding="10")
notebook.add(storage_frame, text="分区/LVM/RAID")
self._create_category_frame(storage_frame, {**summary['categories']['partition'],
**summary['categories']['lvm'],
**summary['categories']['raid']})
# 其他工具页
other_frame = ttk.Frame(notebook, padding="10")
notebook.add(other_frame, text="其他工具")
self._create_category_frame(other_frame, summary['categories']['other'])
# 安装指南页
if summary['install_commands']:
install_frame = ttk.Frame(notebook, padding="10")
notebook.add(install_frame, text="安装指南")
self._create_install_frame(install_frame, summary)
# 底部按钮
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=5)
ttk.Button(btn_frame, text="刷新",
command=self._refresh).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="关闭",
command=self._on_close).pack(side=tk.RIGHT, padx=5)
def _create_category_frame(self, parent, items):
"""创建分类框架"""
canvas = tk.Canvas(parent)
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw", width=580)
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
for key, status in items.items():
self._create_item_frame(scrollable_frame, status)
def _create_item_frame(self, parent, status):
"""创建单个依赖项框架"""
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=3)
is_ok = status['is_complete']
is_optional = status['optional']
# 状态图标
if is_ok:
icon = ""
color = "green"
elif is_optional:
icon = ""
color = "orange"
else:
icon = ""
color = "red"
# 标题行
header_frame = ttk.Frame(frame)
header_frame.pack(fill=tk.X)
ttk.Label(header_frame, text=icon, foreground=color,
font=('Arial', 10, 'bold')).pack(side=tk.LEFT)
ttk.Label(header_frame, text=status['name'],
font=('Arial', 9, 'bold')).pack(side=tk.LEFT, padx=5)
if is_optional:
ttk.Label(header_frame, text="(可选)",
foreground="gray").pack(side=tk.LEFT)
# 详情
detail_frame = ttk.Frame(frame)
detail_frame.pack(fill=tk.X, padx=(20, 0))
if is_ok:
ttk.Label(detail_frame, text=f"已安装命令: {', '.join(status['installed'])}",
foreground="green").pack(anchor=tk.W)
else:
if status['missing']:
ttk.Label(detail_frame, text=f"缺失命令: {', '.join(status['missing'])}",
foreground="red").pack(anchor=tk.W)
ttk.Label(detail_frame, text=f"安装包: {status['package']}",
foreground="blue").pack(anchor=tk.W)
ttk.Separator(frame, orient='horizontal').pack(fill=tk.X, pady=5)
def _create_install_frame(self, parent, summary):
"""创建安装指南框架"""
# 安装命令
cmd_frame = ttk.LabelFrame(parent, text="一键安装命令", padding="10")
cmd_frame.pack(fill=tk.X, pady=(0, 10))
os_type = summary['os_type']
if os_type == 'centos':
cmd = "sudo yum install -y " + " ".join(summary['install_commands'].keys())
elif os_type == 'arch':
cmd = "sudo pacman -Sy --needed " + " ".join(summary['install_commands'].keys())
elif os_type == 'ubuntu':
cmd = "sudo apt-get install -y " + " ".join(summary['install_commands'].keys())
else:
cmd = "# 请根据您的发行版安装以下包: " + ", ".join(summary['install_commands'].keys())
text_widget = tk.Text(cmd_frame, height=3, wrap=tk.WORD,
font=('Consolas', 10))
text_widget.pack(fill=tk.X)
text_widget.insert('1.0', cmd)
text_widget.config(state='disabled')
# 包详情
detail_frame = ttk.LabelFrame(parent, text="包详情", padding="10")
detail_frame.pack(fill=tk.BOTH, expand=True)
canvas = tk.Canvas(detail_frame)
scrollbar = ttk.Scrollbar(detail_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw", width=550)
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
for pkg, names in summary['install_commands'].items():
ttk.Label(scrollable_frame, text=f"{pkg}",
font=('Arial', 9, 'bold')).pack(anchor=tk.W)
ttk.Label(scrollable_frame,
text=f" 支持: {', '.join(names)}",
foreground="gray").pack(anchor=tk.W, padx=(15, 0))
def _refresh(self):
"""刷新检查"""
from dependency_checker import get_dependency_checker
# 清除缓存
checker = get_dependency_checker()
checker._cache = {}
# 重新创建界面
for widget in self.dialog.winfo_children():
widget.destroy()
self._create_widgets()
def _on_close(self):
"""关闭对话框"""
self.dialog.destroy()
def wait_for_result(self):
"""等待对话框关闭"""
self.dialog.wait_window()
class EnhancedFormatDialog:
"""增强格式化对话框 - 显示详细警告信息"""
@@ -19,7 +250,7 @@ class EnhancedFormatDialog:
self.dialog = tk.Toplevel(parent)
self.dialog.title(f"格式化确认 - {device_path}")
self.dialog.geometry("500x575")
self.dialog.geometry("500x500")
self.dialog.minsize(480, 450)
self.dialog.transient(parent)
self.dialog.grab_set()
@@ -28,14 +259,17 @@ class EnhancedFormatDialog:
self._create_widgets()
def _center_window(self):
"""居中窗口"""
self.dialog.update_idletasks()
width = self.dialog.winfo_width()
height = self.dialog.winfo_height()
width = 500
height = 500
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
self.dialog.geometry(f'{width}x{height}+{x}+{y}')
def _create_widgets(self):
"""创建界面元素"""
# 主框架
main_frame = ttk.Frame(self.dialog, padding="15")
main_frame.pack(fill=tk.BOTH, expand=True)
@@ -43,58 +277,45 @@ class EnhancedFormatDialog:
warning_frame = ttk.Frame(main_frame)
warning_frame.pack(fill=tk.X, pady=(0, 15))
warning_label = ttk.Label(
warning_frame,
text="⚠ 警告:此操作将永久删除所有数据!",
font=("Arial", 12, "bold"),
foreground="red"
)
warning_label.pack()
ttk.Label(warning_frame, text="⚠️ 警告",
font=('Arial', 16, 'bold'),
foreground='red').pack(anchor=tk.W)
# 设备信息区域
# 设备信息
info_frame = ttk.LabelFrame(main_frame, text="设备信息", padding="10")
info_frame.pack(fill=tk.X, pady=(0, 15))
# 显示详细信息
infos = [
("设备路径", self.device_path),
("设备名称", self.device_info.get('name', 'N/A')),
("文件系统", self.device_info.get('fstype', 'N/A')),
("大小", self.device_info.get('size', 'N/A')),
("UUID", self.device_info.get('uuid', 'N/A')),
("挂载点", self.device_info.get('mountpoint', '未挂载')),
]
ttk.Label(info_frame,
text=f"设备路径: {self.device_path}",
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=2)
ttk.Label(info_frame,
text=f"设备类型: {self.device_info.get('type', 'Unknown')}",
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
for i, (label, value) in enumerate(infos):
ttk.Label(info_frame, text=f"{label}:", font=("Arial", 10, "bold")).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
val_label = ttk.Label(info_frame, text=str(value))
val_label.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 显示文件系统信息
fstype = self.device_info.get('fstype', '')
if fstype:
ttk.Label(info_frame,
text=f"当前文件系统: {fstype}",
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
# 如果有挂载点,高亮显示
if label == "挂载点" and value and value != '未挂载':
val_label.configure(foreground="orange", font=("Arial", 9, "bold"))
size = self.device_info.get('size', 'Unknown')
ttk.Label(info_frame,
text=f"设备大小: {size}",
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
# 数据风险提示
risk_frame = ttk.LabelFrame(main_frame, text="数据风险提示", padding="10")
risk_frame.pack(fill=tk.X, pady=(0, 15))
# 危险警告
danger_frame = ttk.Frame(main_frame)
danger_frame.pack(fill=tk.X, pady=(0, 15))
risks = []
if self.device_info.get('fstype'):
risks.append(f"• 此分区包含 {self.device_info['fstype']} 文件系统")
if self.device_info.get('uuid'):
risks.append(f"• 分区有 UUID: {self.device_info['uuid'][:20]}...")
if self.device_info.get('mountpoint'):
risks.append(f"• 分区当前挂载在: {self.device_info['mountpoint']}")
ttk.Label(danger_frame,
text="此操作将永久删除设备上的所有数据!",
font=('Arial', 10, 'bold'),
foreground='red').pack(anchor=tk.W, pady=5)
if not risks:
risks.append("• 无法获取分区详细信息")
for risk in risks:
ttk.Label(risk_frame, text=risk, foreground="red").pack(
anchor=tk.W, pady=2
)
ttk.Label(danger_frame,
text="• 所有数据将无法恢复\n• 请确保已备份重要数据",
foreground='red').pack(anchor=tk.W, pady=5)
# 文件系统选择
fs_frame = ttk.LabelFrame(main_frame, text="选择新文件系统", padding="10")
@@ -107,19 +328,6 @@ class EnhancedFormatDialog:
ttk.Radiobutton(fs_frame, text=fs, variable=self.fs_var,
value=fs).pack(side=tk.LEFT, padx=10)
# 确认输入
confirm_frame = ttk.Frame(main_frame)
confirm_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Label(confirm_frame,
text=f"请输入设备名 '{self.device_info.get('name', '确认')}' 以继续:",
foreground="red").pack(anchor=tk.W, pady=5)
self.confirm_var = tk.StringVar()
self.confirm_entry = ttk.Entry(confirm_frame, textvariable=self.confirm_var, width=20)
self.confirm_entry.pack(anchor=tk.W, pady=5)
self.confirm_entry.focus()
# 按钮
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=10)
@@ -130,12 +338,6 @@ class EnhancedFormatDialog:
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
def _on_confirm(self):
expected = self.device_info.get('name', '确认')
if self.confirm_var.get() != expected:
messagebox.showerror("错误",
f"输入错误!请输入 '{expected}' 以确认格式化操作。")
return
self.result = self.fs_var.get()
self.dialog.destroy()
@@ -158,9 +360,9 @@ class EnhancedRaidDeleteDialog:
self.result = False
self.dialog = tk.Toplevel(parent)
self.dialog.title(f"删除 RAID 阵列确认 - {array_path}")
self.dialog.geometry("550x750")
self.dialog.minsize(520, 750)
self.dialog.title(f"删除 RAID 阵列 - {array_path}")
self.dialog.geometry("500x500")
self.dialog.minsize(450, 400)
self.dialog.transient(parent)
self.dialog.grab_set()
@@ -168,96 +370,62 @@ class EnhancedRaidDeleteDialog:
self._create_widgets()
def _center_window(self):
"""居中窗口"""
self.dialog.update_idletasks()
width = self.dialog.winfo_width()
height = self.dialog.winfo_height()
width = 500
height = 500
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
self.dialog.geometry(f'{width}x{height}+{x}+{y}')
def _create_widgets(self):
"""创建界面元素"""
# 主框架
main_frame = ttk.Frame(self.dialog, padding="15")
main_frame.pack(fill=tk.BOTH, expand=True)
# 警告标题
ttk.Label(main_frame, text="⚠️ 确认删除 RAID 阵列",
font=('Arial', 14, 'bold'),
foreground='red').pack(anchor=tk.W, pady=(0, 10))
# 阵列信息
info_frame = ttk.LabelFrame(main_frame, text="阵列信息", padding="10")
info_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(info_frame, text=f"阵列名称: {self.array_path}",
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=2)
ttk.Label(info_frame, text=f"阵列类型: RAID {self.array_data.get('level', 'Unknown')}",
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
ttk.Label(info_frame, text=f"阵列大小: {self.array_data.get('size', 'Unknown')}",
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
# 成员设备
member_frame = ttk.LabelFrame(main_frame, text="成员设备", padding="10")
member_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
for dev in self.member_devices:
ttk.Label(member_frame, text=f"{dev}",
font=('Arial', 9)).pack(anchor=tk.W, pady=1)
# 危险警告
warning_frame = ttk.Frame(main_frame)
warning_frame.pack(fill=tk.X, pady=(0, 15))
danger_frame = ttk.Frame(main_frame)
danger_frame.pack(fill=tk.X, pady=(0, 10))
warning_label = ttk.Label(
warning_frame,
text="⚠ 危险操作:此操作将永久删除 RAID 阵列!",
font=("Arial", 13, "bold"),
foreground="red"
)
warning_label.pack()
ttk.Label(danger_frame,
text="⚠️ 删除操作将:",
font=('Arial', 10, 'bold'),
foreground='red').pack(anchor=tk.W, pady=5)
# 阵列详情
array_frame = ttk.LabelFrame(main_frame, text="阵列详情", padding="10")
array_frame.pack(fill=tk.X, pady=(0, 15))
details = [
("阵列设备", self.array_path),
("RAID 级别", self.array_data.get('level', 'N/A')),
("阵列状态", self.array_data.get('state', 'N/A')),
("阵列大小", self.array_data.get('array_size', 'N/A')),
("UUID", self.array_data.get('uuid', 'N/A')),
("Chunk 大小", self.array_data.get('chunk_size', 'N/A')),
warnings = [
"• 停止并删除 RAID 阵列",
"• 清除成员设备上的 RAID 超级块",
"• 从配置文件中移除阵列配置",
"• 所有数据将永久丢失且无法恢复"
]
for i, (label, value) in enumerate(details):
ttk.Label(array_frame, text=f"{label}:",
font=("Arial", 10, "bold")).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
ttk.Label(array_frame, text=str(value)).grid(
row=i, column=1, sticky=tk.W, padx=5, pady=3
)
# 成员设备列表
member_frame = ttk.LabelFrame(main_frame, text="成员设备(将被清除 RAID 超级块)",
padding="10")
member_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
if self.member_devices:
for i, dev in enumerate(self.member_devices):
if dev:
ttk.Label(member_frame, text=f"{dev}",
font=("Arial", 10), foreground="red").pack(
anchor=tk.W, pady=2
)
else:
ttk.Label(member_frame, text=" 无法获取成员设备列表",
foreground="orange").pack(anchor=tk.W, pady=5)
# 后果警告
consequence_frame = ttk.LabelFrame(main_frame, text="操作后果", padding="10")
consequence_frame.pack(fill=tk.X, pady=(0, 15))
consequences = [
"• 阵列将被停止并从系统中移除",
"• 所有成员设备的 RAID 超级块将被清除",
"• 阵列上的所有数据将永久丢失",
"• /etc/mdadm.conf 中的配置将被删除",
"• 此操作无法撤销!"
]
for con in consequences:
ttk.Label(consequence_frame, text=con,
foreground="red").pack(anchor=tk.W, pady=2)
# 二次确认
confirm_frame = ttk.Frame(main_frame)
confirm_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(confirm_frame,
text="请输入 'DELETE' 以确认删除操作:",
foreground="red", font=("Arial", 10, "bold")).pack(
anchor=tk.W, pady=5)
self.confirm_var = tk.StringVar()
self.confirm_entry = ttk.Entry(confirm_frame, textvariable=self.confirm_var, width=15)
self.confirm_entry.pack(anchor=tk.W, pady=5)
self.confirm_entry.focus()
for warning in warnings:
ttk.Label(danger_frame, text=warning,
foreground='red').pack(anchor=tk.W, pady=2)
# 按钮
btn_frame = ttk.Frame(main_frame)
@@ -266,23 +434,14 @@ class EnhancedRaidDeleteDialog:
ttk.Button(btn_frame, text="取消",
command=self._on_cancel).pack(side=tk.RIGHT, padx=5)
ttk.Button(btn_frame, text="确认删除",
command=self._on_confirm,
style="Danger.TButton").pack(side=tk.RIGHT, padx=5)
# 配置危险按钮样式
style = ttk.Style()
style.configure("Danger.TButton", foreground="red")
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
def _on_confirm(self):
if self.confirm_var.get() != "DELETE":
messagebox.showerror("错误",
"输入错误!请输入 'DELETE'(全大写)以确认删除操作。")
return
self.result = True
self.dialog.destroy()
def _on_cancel(self):
self.result = False
self.dialog.destroy()
def wait_for_result(self):
@@ -291,7 +450,7 @@ class EnhancedRaidDeleteDialog:
class EnhancedPartitionDialog:
"""增强分区创建对话框 - 可视化滑块选择大小"""
"""增强分区创建对话框 - 智能分区建议"""
def __init__(self, parent, disk_path, total_disk_mib, max_available_mib):
self.parent = parent
@@ -302,8 +461,8 @@ class EnhancedPartitionDialog:
self.dialog = tk.Toplevel(parent)
self.dialog.title(f"创建分区 - {disk_path}")
self.dialog.geometry("550x575")
self.dialog.minsize(500, 575)
self.dialog.geometry("550x550")
self.dialog.minsize(500, 500)
self.dialog.transient(parent)
self.dialog.grab_set()
@@ -311,90 +470,99 @@ class EnhancedPartitionDialog:
self._create_widgets()
def _center_window(self):
"""居中窗口"""
self.dialog.update_idletasks()
width = self.dialog.winfo_width()
height = self.dialog.winfo_height()
width = 550
height = 550
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
self.dialog.geometry(f'{width}x{height}+{x}+{y}')
def _create_widgets(self):
"""创建界面元素"""
# 主框架
main_frame = ttk.Frame(self.dialog, padding="15")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
ttk.Label(main_frame, text=f"{self.disk_path} 上创建分区",
font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# 磁盘信息
info_frame = ttk.LabelFrame(main_frame, text="磁盘信息", padding="10")
info_frame.pack(fill=tk.X, pady=(0, 15))
info_frame.pack(fill=tk.X, pady=(0, 10))
total_gb = self.total_disk_mib / 1024
max_gb = self.max_available_mib / 1024
available_gb = self.max_available_mib / 1024
ttk.Label(info_frame, text=f"磁盘: {self.disk_path}").pack(anchor=tk.W, pady=2)
ttk.Label(info_frame, text=f"总容量: {total_gb:.2f} GB").pack(anchor=tk.W, pady=2)
ttk.Label(info_frame, text=f"可用空间: {max_gb:.2f} GB",
foreground="green").pack(anchor=tk.W, pady=2)
ttk.Label(info_frame,
text=f"磁盘总大小: {total_gb:.2f} GB").pack(anchor=tk.W)
ttk.Label(info_frame,
text=f"最大可用空间: {available_gb:.2f} GB",
foreground='green' if available_gb > 1 else 'red').pack(anchor=tk.W)
# 分区表类型
pt_frame = ttk.LabelFrame(main_frame, text="分区表类型", padding="10")
pt_frame.pack(fill=tk.X, pady=(0, 15))
# 分区表类型选择
part_type_frame = ttk.LabelFrame(main_frame, text="分区表类型", padding="10")
part_type_frame.pack(fill=tk.X, pady=(0, 10))
self.part_type_var = tk.StringVar(value="gpt")
ttk.Radiobutton(pt_frame, text="GPT (推荐,支持大容量磁盘)",
variable=self.part_type_var, value="gpt").pack(anchor=tk.W, pady=2)
ttk.Radiobutton(pt_frame, text="MBR/MSDOS (兼容旧系统)",
variable=self.part_type_var, value="msdos").pack(anchor=tk.W, pady=2)
ttk.Radiobutton(part_type_frame, text="GPT (推荐,支持2TB以上磁盘)",
variable=self.part_type_var,
value="gpt").pack(anchor=tk.W, pady=2)
ttk.Radiobutton(part_type_frame, text="MBR (传统格式,兼容性更好)",
variable=self.part_type_var,
value="msdos").pack(anchor=tk.W, pady=2)
# 大小选择
# 大小设置
size_frame = ttk.LabelFrame(main_frame, text="分区大小", padding="10")
size_frame.pack(fill=tk.X, pady=(0, 15))
size_frame.pack(fill=tk.X, pady=(0, 10))
# 使用最大空间复选框
self.use_max_var = tk.BooleanVar(value=True)
ttk.Checkbutton(size_frame, text="使用最大可用空间",
# 使用全部空间选项
self.use_max_var = tk.BooleanVar(value=False)
ttk.Checkbutton(size_frame,
text="使用全部可用空间",
variable=self.use_max_var,
command=self._toggle_size_input).pack(anchor=tk.W, pady=5)
# 滑块和输入框容器
self.size_control_frame = ttk.Frame(size_frame)
self.size_control_frame.pack(fill=tk.X, pady=5)
# 大小输入
size_input_frame = ttk.Frame(size_frame)
size_input_frame.pack(fill=tk.X, pady=5)
# 滑块
self.size_var = tk.DoubleVar(value=max_gb)
self.size_slider = ttk.Scale(
self.size_control_frame,
from_=0.1,
to=max_gb,
orient=tk.HORIZONTAL,
variable=self.size_var,
command=self._on_slider_change
)
self.size_slider.pack(fill=tk.X, pady=5)
ttk.Label(size_input_frame, text="大小 (GB):").pack(side=tk.LEFT)
# 输入框和标签
input_frame = ttk.Frame(self.size_control_frame)
input_frame.pack(fill=tk.X, pady=5)
ttk.Label(input_frame, text="大小:").pack(side=tk.LEFT, padx=5)
self.size_spin = tk.Spinbox(
input_frame,
from_=0.1,
to=max_gb,
increment=0.1,
textvariable=self.size_var,
width=10,
command=self._on_spin_change
)
self.size_var = tk.DoubleVar(value=min(available_gb, 100))
# 使用 tk.Spinbox 兼容旧版 tkinter (Python 3.6)
try:
self.size_spin = tk.Spinbox(size_input_frame,
from_=0.1,
to=available_gb,
textvariable=self.size_var,
width=10,
increment=0.1)
except AttributeError:
# 如果 tk.Spinbox 也不可用,使用 Entry 替代
self.size_spin = tk.Entry(size_input_frame,
textvariable=self.size_var,
width=10)
self.size_spin.pack(side=tk.LEFT, padx=5)
ttk.Label(input_frame, text="GB").pack(side=tk.LEFT, padx=5)
# 滑块
self.size_slider = ttk.Scale(size_frame,
from_=0,
to=available_gb,
orient=tk.HORIZONTAL,
length=400)
self.size_slider.set(min(available_gb, 100))
self.size_slider.pack(fill=tk.X, pady=5)
# 显示百分比
self.percent_label = ttk.Label(input_frame, text="(100%)")
self.percent_label.pack(side=tk.LEFT, padx=10)
# 百分比显示
self.percent_var = tk.StringVar(value="0%")
ttk.Label(size_frame, textvariable=self.percent_var,
foreground='gray').pack(anchor=tk.E)
# 可视化空间显示
self._create_space_visualizer(main_frame, max_gb)
# 绑定更新事件
self.size_var.trace('w', self._on_size_change)
self.size_slider.configure(command=self._on_slider_change)
# 初始状态
self._toggle_size_input()
@@ -405,90 +573,49 @@ class EnhancedPartitionDialog:
ttk.Button(btn_frame, text="取消",
command=self._on_cancel).pack(side=tk.RIGHT, padx=5)
ttk.Button(btn_frame, text="创建分区",
ttk.Button(btn_frame, text="确认创建",
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
def _create_space_visualizer(self, parent, max_gb):
"""创建空间可视化条"""
viz_frame = ttk.LabelFrame(parent, text="空间使用预览", padding="10")
viz_frame.pack(fill=tk.X, pady=(0, 15))
# Canvas 用于绘制
self.viz_canvas = tk.Canvas(viz_frame, height=40, bg="white")
self.viz_canvas.pack(fill=tk.X, pady=5)
# 等待 Canvas 渲染完成后再更新
self.viz_canvas.update_idletasks()
# 绘制初始状态
self._update_visualizer(max_gb, max_gb)
def _update_visualizer(self, current_gb, max_gb):
"""更新空间可视化"""
self.viz_canvas.delete("all")
width = self.viz_canvas.winfo_width() or 400
height = self.viz_canvas.winfo_height() or 40
# 计算比例
if max_gb > 0:
ratio = current_gb / max_gb
else:
ratio = 0
fill_width = int(width * ratio)
# 绘制背景
self.viz_canvas.create_rectangle(0, 0, width, height, fill="lightgray", outline="")
# 绘制已用空间
if fill_width > 0:
color = "green" if ratio < 0.8 else "orange" if ratio < 0.95 else "red"
self.viz_canvas.create_rectangle(0, 0, fill_width, height, fill=color, outline="")
# 绘制文字
self.viz_canvas.create_text(
width // 2, height // 2,
text=f"{current_gb:.2f} GB / {max_gb:.2f} GB",
font=("Arial", 10, "bold")
)
def _on_size_change(self, *args):
"""大小输入变化时更新滑块"""
try:
value = self.size_var.get()
self.size_slider.set(value)
self._update_percent(value)
except:
pass
def _on_slider_change(self, value):
"""滑块变化回调"""
"""滑块变化时更新输入"""
try:
gb = float(value)
self._update_percent(gb)
self._update_visualizer(gb, self.max_available_mib / 1024)
self.size_var.set(float(value))
self._update_percent(float(value))
except:
pass
def _on_spin_change(self):
"""输入框变化回调"""
try:
gb = self.size_var.get()
self._update_percent(gb)
self._update_visualizer(gb, self.max_available_mib / 1024)
except:
pass
def _update_percent(self, gb):
def _update_percent(self, value):
"""更新百分比显示"""
max_gb = self.max_available_mib / 1024
if max_gb > 0:
percent = (gb / max_gb) * 100
self.percent_label.configure(text=f"({percent:.1f}%)")
if self.max_available_mib > 0:
percent = (value * 1024 / self.max_available_mib) * 100
self.percent_var.set(f"{percent:.1f}%")
def _toggle_size_input(self):
"""切换大小输入状态"""
if self.use_max_var.get():
self.size_slider.configure(state=tk.DISABLED)
# ttk.Scale 在某些旧版本 Tkinter 中不支持 state 选项
try:
self.size_slider.configure(state=tk.DISABLED)
except tk.TclError:
pass # 忽略不支持的选项
self.size_spin.configure(state=tk.DISABLED)
max_gb = self.max_available_mib / 1024
self.size_var.set(max_gb)
self._update_percent(max_gb)
self._update_visualizer(max_gb, max_gb)
else:
self.size_slider.configure(state=tk.NORMAL)
try:
self.size_slider.configure(state=tk.NORMAL)
except tk.TclError:
pass # 忽略不支持的选项
self.size_spin.configure(state=tk.NORMAL)
def _on_confirm(self):
@@ -508,8 +635,8 @@ class EnhancedPartitionDialog:
'disk_path': self.disk_path,
'partition_table_type': self.part_type_var.get(),
'size_gb': size_gb,
'total_disk_mib': self.total_disk_mib,
'use_max_space': use_max_space
'use_max_space': use_max_space,
'total_disk_mib': self.total_disk_mib
}
self.dialog.destroy()

View File

@@ -19,11 +19,13 @@ class SmartInfoDialog:
self.dialog.title(f"SMART 信息 - {device_path}")
self.dialog.geometry("700x600")
self.dialog.transient(parent)
self.dialog.grab_set()
self._center_window()
self._create_widgets()
# 窗口可见后再设置 grab_set
self.dialog.after(100, self.dialog.grab_set)
def _center_window(self):
"""居中显示"""
self.dialog.update_idletasks()
@@ -213,12 +215,14 @@ class SmartOverviewDialog:
self.dialog.title("SMART 健康监控 - 总览")
self.dialog.geometry("800x500")
self.dialog.transient(parent)
self.dialog.grab_set()
self._center_window()
self._create_widgets()
self._refresh_data()
# 窗口可见后再设置 grab_set
self.dialog.after(100, self.dialog.grab_set)
def _center_window(self):
"""居中显示"""
self.dialog.update_idletasks()

72
disk-manager-compat.spec Normal file
View File

@@ -0,0 +1,72 @@
# -*- mode: python ; coding: utf-8 -*-
"""
Linux 存储管理器 - PyInstaller 打包配置 (Tkinter 版本)
支持 Arch Linux CentOS/RHEL 8
"""
import sys
import os
block_cipher = None
# 分析依赖
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[
# 平台兼容模块
'platform_compat',
'system_compat',
# Tkinter 相关
'tkinter',
'tkinter.ttk',
'tkinter.messagebox',
'tkinter.filedialog',
'tkinter.simpledialog',
# 其他依赖
'pexpect',
'ptyprocess',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
# 排除不需要的 GUI 库以减小体积
'PySide6',
'PySide2',
'PyQt5',
'PyQt6',
'matplotlib',
'numpy',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='disk-manager',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@@ -210,7 +210,7 @@ class DiskOperations:
else:
messagebox.showerror("错误", f"挂载设备 {device_path} 失败。\n\n"
f"普通挂载和 ntfs-3g 挂载均失败。\n"
f"如果是 NTFS 文件系统损坏,请先使用文件系统工具中的 "修复文件系统 (ntfsfix)" 修复。")
"如果是 NTFS 文件系统损坏,请先使用文件系统工具中的 '修复文件系统 (ntfsfix)' 修复。")
return False
else:
messagebox.showerror("错误", f"挂载设备 {device_path} 失败: {stderr}")
@@ -308,11 +308,11 @@ class DiskOperations:
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"),
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognised disk label"),
root_privilege=True
)
if not success_check:
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")):
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognised disk label")):
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
has_partition_table = False
else:
@@ -538,7 +538,7 @@ class DiskOperations:
try:
result = subprocess.run(
["losetup", "-a"],
capture_output=True, text=True, check=False
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=False
)
if result.returncode == 0:
for line in result.stdout.splitlines():

View File

@@ -1,38 +1,40 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['mainwindow_tkinter.py'],
pathex=[],
binaries=[('/usr/lib/python3.14/lib-dynload/_tkinter.cpython-314-x86_64-linux-gnu.so', '.')],
datas=[('/usr/lib', 'tcl'), ('/usr/lib/tcl8.6', 'tk'), ('/usr/lib64/tcl8.6', 'tcltk'), ('/usr/lib/tcl8.6', 'tcltk')],
hiddenimports=['tkinter', 'tkinter.ttk', 'tkinter.scrolledtext', 'tkinter.messagebox', 'tkinter.filedialog', 'tkinter.simpledialog', '_tkinter'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
block_cipher = None
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='linux-storage-manager',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
a = Analysis(['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='linux-storage-manager',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )

View File

@@ -39,11 +39,12 @@ class LvmOperations:
try:
result = subprocess.run(
command_list,
capture_output=True,
text=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
input=input_data
check=True,
input=input_data,
stdin=subprocess.PIPE if input_data else None
)
logger.info(f"命令成功: {full_cmd_str}")
return True, result.stdout.strip(), result.stderr.strip()

View File

@@ -31,8 +31,9 @@ class LvmOperations:
else:
result = subprocess.run(
["sudo"] + command_list if root_privilege else command_list,
capture_output=True, text=True, check=True,
input=input_data
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=True,
input=input_data,
stdin=subprocess.PIPE if input_data else None
)
stdout = result.stdout
stderr = result.stderr

131
main.py Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Linux 存储管理器 - 统一入口 (Tkinter 版本)
统一使用 Tkinter GUI 后端
支持 Arch Linux 和 CentOS/RHEL 8
"""
import sys
import os
import logging
# 确保可以导入本地模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from platform_compat import check_prerequisites
def setup_logging():
"""配置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
def show_error_dialog(title, message):
"""显示错误对话框"""
print(f"\n{'='*60}")
print(f"错误: {title}")
print(f"{'='*60}")
print(message)
print(f"{'='*60}\n")
# 使用 tkinter 显示图形错误对话框
try:
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
messagebox.showerror(title, message)
root.destroy()
except Exception:
pass
def get_installation_guide():
"""获取安装指南"""
return """安装指南:
Arch Linux:
sudo pacman -S python python-tkinter parted mdadm lvm2
sudo pacman -S dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
CentOS 8 / RHEL 8:
sudo yum install -y python3 python3-tkinter parted mdadm lvm2
sudo yum install -y dosfstools e2fsprogs xfsprogs ntfs-3g
pip3 install pexpect
"""
def main():
"""主入口函数"""
setup_logging()
logger = logging.getLogger(__name__)
logger.info("="*60)
logger.info("Linux 存储管理器启动 (Tkinter)")
logger.info("="*60)
# 检查 tkinter 是否可用
try:
import tkinter as tk
logger.info("Tkinter 已加载")
except ImportError as e:
show_error_dialog(
"启动失败",
f"无法加载 Tkinter: {e}\n\n"
"请安装 tkinter:\n"
" CentOS 8: sudo yum install python3-tkinter\n"
" Arch Linux: sudo pacman -S tk\n"
)
sys.exit(1)
# 检查前提条件
passed, errors, warnings = check_prerequisites()
if not passed:
error_msg = "系统缺少必要的依赖:\n\n"
for e in errors:
error_msg += f"{e}\n"
error_msg += "\n" + get_installation_guide()
show_error_dialog("启动失败", error_msg)
sys.exit(1)
# 显示警告
for w in warnings:
logger.warning(w)
logger.info("启动 Tkinter 主窗口")
try:
# 统一使用 Tkinter 版本
from mainwindow_tkinter import MainWindow
root = tk.Tk()
app = MainWindow(root)
root.mainloop()
except ImportError as e:
logger.exception("导入模块失败")
show_error_dialog(
"启动失败",
f"无法加载 GUI 模块: {e}\n\n"
"请检查依赖是否正确安装。\n\n" +
get_installation_guide()
)
sys.exit(1)
except Exception as e:
logger.exception("启动过程中发生错误")
show_error_dialog("启动失败", f"发生错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -26,7 +26,8 @@ from dialogs_tkinter import (CreatePartitionDialog, MountDialog, CreateRaidDialo
from dialogs_smart import SmartInfoDialog, SmartOverviewDialog
from smart_monitor import SmartMonitor
# 导入增强对话框
from dialogs_enhanced import EnhancedFormatDialog, EnhancedRaidDeleteDialog, EnhancedPartitionDialog
from dialogs_enhanced import (EnhancedFormatDialog, EnhancedRaidDeleteDialog,
EnhancedPartitionDialog, DependencyCheckDialog)
class MainWindow:
@@ -138,6 +139,8 @@ class MainWindow:
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="依赖检查", command=self._show_dependency_check)
help_menu.add_separator()
help_menu.add_command(label="关于", command=self._show_about)
def _show_about(self):
@@ -155,6 +158,10 @@ class MainWindow:
"使用 Python 3.6+ 和 Tkinter 构建"
)
def _show_dependency_check(self):
"""显示依赖检查对话框"""
DependencyCheckDialog(self.root)
def _create_block_devices_tree(self):
"""创建块设备 Treeview"""
# 创建框架
@@ -407,6 +414,48 @@ class MainWindow:
menu.add_command(label=f"擦除分区表 {device_path}...",
command=lambda: self._handle_wipe_partition_table(device_path))
menu.add_separator()
# 检查磁盘状态
has_children = dev_data.get('children') is not None and len(dev_data.get('children', [])) > 0
fstype = dev_data.get('fstype', '')
# 如果磁盘有文件系统(直接格式化的物理盘),添加挂载/卸载/格式化操作
if fstype and fstype not in ['LVM2_member', 'linux_raid_member']:
if not mount_point or mount_point == '' or mount_point == 'N/A':
menu.add_command(label=f"挂载 {device_path}...",
command=lambda: self._handle_mount(device_path))
else:
menu.add_command(label=f"卸载 {device_path}",
command=lambda: self._unmount_and_refresh(device_path))
menu.add_separator()
# 格式化物理盘
menu.add_command(label=f"格式化物理盘 {device_path}...",
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
# 文件系统工具
fs_menu = tk.Menu(menu, tearoff=0)
if fstype.startswith('ext'):
fs_menu.add_command(label=f"检查文件系统 (fsck) {device_path}",
command=lambda dp=device_path, ft=fstype: self._handle_fsck(dp, ft))
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {device_path}",
command=lambda dp=device_path, ft=fstype: self._handle_resize2fs(dp, ft))
elif fstype == 'xfs':
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {device_path}",
command=lambda dp=device_path: self._handle_xfs_repair(dp))
elif fstype.lower() == 'ntfs':
fs_menu.add_command(label=f"修复文件系统 (ntfsfix) {device_path}",
command=lambda dp=device_path: self._handle_ntfsfix(dp))
if fs_menu.index(tk.END) is not None:
menu.add_cascade(label=f"文件系统工具 {device_path}", menu=fs_menu)
menu.add_separator()
elif not has_children:
# 没有文件系统、没有子分区的空物理盘,可以直接格式化
menu.add_command(label=f"直接格式化物理盘 {device_path}...",
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
menu.add_separator()
# SMART 信息
menu.add_command(label=f"查看 SMART 信息 {device_path}",
command=lambda: self._handle_show_smart(device_path))
@@ -1092,8 +1141,17 @@ class MainWindow:
self.refresh_all_info()
def _handle_format_raid_array(self, array_path):
"""处理格式化 RAID 阵列"""
self.disk_ops.format_partition(array_path)
"""处理格式化 RAID 阵列 - 使用增强型对话框"""
# 获取设备信息
dev_data = self.system_manager.get_device_details_by_path(array_path)
# 使用增强型格式化对话框
dialog = EnhancedFormatDialog(self.root, array_path, dev_data or {})
fs_type = dialog.wait_for_result()
if fs_type:
# 执行格式化
self.disk_ops.format_partition(array_path, fs_type)
# === LVM 处理函数 ===
def refresh_lvm_info(self):

View File

@@ -115,7 +115,7 @@ class OccupationResolver:
try:
result = subprocess.run(
["sudo", "lsof", device_path],
capture_output=True, text=True, timeout=10
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', timeout=10
)
if result.returncode == 0 and result.stdout.strip():
logger.info(f"设备 {device_path} 被以下进程占用:\n{result.stdout}")
@@ -127,7 +127,7 @@ class OccupationResolver:
try:
result = subprocess.run(
["sudo", "fuser", device_path],
capture_output=True, text=True, timeout=10
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', timeout=10
)
if result.returncode == 0 and result.stdout.strip():
logger.info(f"设备 {device_path} 被以下进程占用: {result.stdout.strip()}")
@@ -149,7 +149,7 @@ class OccupationResolver:
# 使用 fuser -k 终止进程
result = subprocess.run(
["sudo", "fuser", "-k", device_path],
capture_output=True, text=True, timeout=10
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', timeout=10
)
if result.returncode == 0 or result.returncode == 1: # 1 表示没有进程被终止

View File

@@ -203,8 +203,9 @@ class PartitionTableBackup:
# 使用 sfdisk 备份分区表
result = subprocess.run(
["sudo", "sfdisk", "-d", device_path],
capture_output=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
check=True,
timeout=30
)
@@ -236,8 +237,10 @@ class PartitionTableBackup:
result = subprocess.run(
["sudo", "sfdisk", device_path],
input=partition_data,
capture_output=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
encoding='utf-8',
check=True,
timeout=30
)

337
platform_compat.py Executable file
View File

@@ -0,0 +1,337 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
平台兼容性检测模块
自动检测操作系统和可用库,提供统一的兼容性接口
支持 Arch Linux (PySide6) 和 CentOS 8 (Tkinter)
"""
import sys
import os
import subprocess
import platform
import logging
logger = logging.getLogger(__name__)
class PlatformCompat:
"""平台兼容性检测类"""
# GUI 后端类型
GUI_PYSIDE6 = "pyside6"
GUI_TKINTER = "tkinter"
GUI_NONE = "none"
# 操作系统类型
OS_ARCH = "arch"
OS_CENTOS = "centos"
OS_RHEL = "rhel"
OS_UBUNTU = "ubuntu"
OS_DEBIAN = "debian"
OS_UNKNOWN = "unknown"
def __init__(self):
self._gui_backend = None
self._os_type = None
self._os_version = None
self._python_version = sys.version_info
self._detected = False
def detect(self):
"""执行平台检测"""
if self._detected:
return
logger.info(f"Python版本: {self._python_version.major}.{self._python_version.minor}.{self._python_version.micro}")
logger.info(f"平台: {platform.platform()}")
logger.info(f"系统: {platform.system()}")
logger.info(f"发行版: {platform.release()}")
# 检测操作系统
self._detect_os()
# 检测GUI后端
self._detect_gui_backend()
self._detected = True
logger.info(f"检测结果 - OS: {self._os_type}, GUI后端: {self._gui_backend}")
def _detect_os(self):
"""检测操作系统类型"""
try:
# 尝试读取 /etc/os-release
if os.path.exists('/etc/os-release'):
with open('/etc/os-release', 'r') as f:
content = f.read().lower()
if 'arch' in content:
self._os_type = self.OS_ARCH
elif 'centos' in content:
self._os_type = self.OS_CENTOS
elif 'rhel' in content or 'red hat' in content:
self._os_type = self.OS_RHEL
elif 'ubuntu' in content:
self._os_type = self.OS_UBUNTU
elif 'debian' in content:
self._os_type = self.OS_DEBIAN
else:
self._os_type = self.OS_UNKNOWN
else:
# 尝试使用 platform.linux_distribution() (Python 3.6 可用)
distro = platform.dist() if hasattr(platform, 'dist') else (None, None, None)
if distro[0]:
distro_name = distro[0].lower()
if 'centos' in distro_name or 'rhel' in distro_name:
self._os_type = self.OS_CENTOS
elif 'arch' in distro_name:
self._os_type = self.OS_ARCH
else:
self._os_type = self.OS_UNKNOWN
else:
self._os_type = self.OS_UNKNOWN
except Exception as e:
logger.warning(f"检测操作系统失败: {e}")
self._os_type = self.OS_UNKNOWN
def _detect_gui_backend(self):
"""检测可用的GUI后端"""
# 首先尝试 PySide6 (需要 Python 3.7+)
if self._python_version >= (3, 7):
try:
import PySide6
self._gui_backend = self.GUI_PYSIDE6
logger.info("检测到 PySide6 可用")
return
except ImportError:
logger.debug("PySide6 不可用")
else:
logger.debug(f"Python {self._python_version.major}.{self._python_version.minor} 不支持 PySide6 (需要 3.7+)")
# 尝试 PySide2 (CentOS 8 可能可用)
try:
import PySide2
self._gui_backend = "pyside2"
logger.info("检测到 PySide2 可用")
return
except ImportError:
logger.debug("PySide2 不可用")
# 最后尝试 Tkinter (Python 内置)
try:
import tkinter
self._gui_backend = self.GUI_TKINTER
logger.info("检测到 Tkinter 可用")
return
except ImportError:
logger.debug("Tkinter 不可用")
self._gui_backend = self.GUI_NONE
logger.error("没有检测到可用的GUI后端")
def get_gui_backend(self):
"""获取检测到的GUI后端"""
if not self._detected:
self.detect()
return self._gui_backend
def get_os_type(self):
"""获取检测到的操作系统类型"""
if not self._detected:
self.detect()
return self._os_type
def is_pyside6_available(self):
"""检查 PySide6 是否可用"""
return self.get_gui_backend() == self.GUI_PYSIDE6
def is_tkinter_available(self):
"""检查 Tkinter 是否可用"""
return self.get_gui_backend() == self.GUI_TKINTER
def get_recommended_entry_point(self):
"""
获取推荐的入口点模块名
统一使用 Tkinter 版本
返回: (模块名, 类名)
"""
return ("mainwindow_tkinter", "MainWindow")
def get_system_command_compat(self):
"""
获取系统命令兼容性配置
不同发行版的命令参数可能有差异
"""
os_type = self.get_os_type()
# 默认配置
config = {
'lsblk_json_available': True, # lsblk -J 支持
'parted_script_mode': True, # parted -s 支持
'lvm_json_report': True, # LVM JSON 报告格式
'mdadm_conf_paths': [
'/etc/mdadm/mdadm.conf',
'/etc/mdadm.conf'
],
}
# CentOS/RHEL 8 特定配置
if os_type in (self.OS_CENTOS, self.OS_RHEL):
# CentOS 8 的 lsblk 支持 -J但某些旧版本可能有问题
# 检查实际版本
try:
result = subprocess.run(['lsblk', '--version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
version_output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
# util-linux 2.23+ 支持 JSON
if '2.23' in version_output or '2.3' in version_output:
config['lsblk_json_available'] = True
else:
# 尝试解析版本号
import re
version_match = re.search(r'(\d+)\.(\d+)', version_output)
if version_match:
major = int(version_match.group(1))
minor = int(version_match.group(2))
config['lsblk_json_available'] = (major > 2) or (major == 2 and minor >= 23)
except Exception:
pass
# CentOS 8 的 LVM 版本支持 JSON 报告
try:
result = subprocess.run(['lvs', '--version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# LVM 2.2.02.166+ 支持 JSON
version_output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
import re
version_match = re.search(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', version_output)
if version_match:
major = int(version_match.group(1))
minor = int(version_match.group(2))
config['lvm_json_report'] = (major >= 2) and (minor >= 2)
except Exception:
pass
return config
def get_installation_guide(self):
"""获取当前系统的安装指南"""
os_type = self.get_os_type()
backend = self.get_gui_backend()
guides = []
if os_type == self.OS_CENTOS:
guides.append("CentOS 8 安装命令:")
guides.append(" sudo yum install -y python3 python3-tkinter parted mdadm lvm2")
guides.append(" sudo yum install -y dosfstools e2fsprogs xfsprogs ntfs-3g")
if backend == self.GUI_TKINTER:
guides.append(" sudo pip3 install pexpect")
elif os_type == self.OS_ARCH:
guides.append("Arch Linux 安装命令:")
guides.append(" sudo pacman -S python python-pyside6 parted mdadm lvm2")
guides.append(" sudo pacman -S dosfstools e2fsprogs xfsprogs ntfs-3g")
guides.append(" pip install pexpect")
else:
guides.append("通用安装命令:")
guides.append(" 请安装: python3, tkinter, parted, mdadm, lvm2")
guides.append(" 以及文件系统工具: e2fsprogs, xfsprogs, dosfstools, ntfs-3g")
return "\n".join(guides)
# 全局实例
_platform_compat = None
def get_platform_compat():
"""获取全局平台兼容性实例"""
global _platform_compat
if _platform_compat is None:
_platform_compat = PlatformCompat()
_platform_compat.detect()
return _platform_compat
def check_prerequisites():
"""
检查系统前提条件
返回: (是否通过, 错误信息列表)
"""
compat = get_platform_compat()
errors = []
warnings = []
# 检查 GUI 后端
if compat.get_gui_backend() == PlatformCompat.GUI_NONE:
errors.append("没有检测到可用的GUI后端 (PySide6/PySide2/Tkinter)")
# 检查必要的系统命令
required_commands = [
('lsblk', '用于获取块设备信息'),
('parted', '用于分区操作'),
('mkfs.ext4', '用于创建ext4文件系统'),
('mount', '用于挂载操作'),
]
optional_commands = [
('mdadm', '用于RAID管理'),
('pvcreate', '用于LVM物理卷管理'),
('vgcreate', '用于LVM卷组管理'),
('lvcreate', '用于LVM逻辑卷管理'),
('mkfs.xfs', '用于创建XFS文件系统'),
('mkfs.ntfs', '用于创建NTFS文件系统'),
('mkfs.vfat', '用于创建FAT32文件系统'),
]
for cmd, desc in required_commands:
if not _command_exists(cmd):
errors.append(f"缺少必要命令: {cmd} ({desc})")
for cmd, desc in optional_commands:
if not _command_exists(cmd):
warnings.append(f"缺少可选命令: {cmd} ({desc})")
# 检查权限
if os.geteuid() != 0:
warnings.append("当前不是root用户部分功能可能需要sudo权限")
return (len(errors) == 0, errors, warnings)
def _command_exists(cmd):
"""检查命令是否存在"""
try:
# Python 3.6 兼容: 使用 stdout/stderr 参数代替 capture_output
result = subprocess.run(
['which', cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
if __name__ == "__main__":
# 测试模式
logging.basicConfig(level=logging.DEBUG)
compat = get_platform_compat()
print(f"操作系统类型: {compat.get_os_type()}")
print(f"GUI后端: {compat.get_gui_backend()}")
print(f"推荐入口点: {compat.get_recommended_entry_point()}")
print(f"系统命令兼容配置: {compat.get_system_command_compat()}")
print("\n" + compat.get_installation_guide())
print("\n前提条件检查:")
passed, errors, warnings = check_prerequisites()
if passed:
print("✓ 基本前提条件满足")
else:
print("✗ 前提条件不满足:")
for e in errors:
print(f" 错误: {e}")
for w in warnings:
print(f" 警告: {w}")

19
run.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Linux 存储管理器启动脚本
# 自动检测并启动合适的版本
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
if [ -f "$SCRIPT_DIR/dist/disk-manager" ]; then
# 打包版本
echo "启动打包版本..."
exec "$SCRIPT_DIR/dist/disk-manager" "$@"
elif [ -f "$SCRIPT_DIR/main.py" ]; then
# 统一入口
echo "启动源码版本..."
exec python3 "$SCRIPT_DIR/main.py" "$@"
else
echo "错误: 找不到 disk-manager 或 main.py"
exit 1
fi

View File

@@ -5,10 +5,38 @@ import subprocess
import logging
import re
import json
import sys
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
# Python 3.6 兼容性: dataclasses 是 3.7+ 的特性
if sys.version_info >= (3, 7):
from dataclasses import dataclass
else:
# Python 3.6 回退: 使用普通类
def dataclass(cls):
"""简化的 dataclass 装饰器兼容层"""
# 自动添加 __init__
annotations = getattr(cls, '__annotations__', {})
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
# 设置默认值
for key in annotations:
if not hasattr(self, key):
setattr(self, key, None)
cls.__init__ = __init__
# 添加 __repr__
def __repr__(self):
fields = ', '.join(f'{k}={getattr(self, k, None)!r}' for k in annotations)
return f"{cls.__name__}({fields})"
cls.__repr__ = __repr__
return cls
logger = logging.getLogger(__name__)
@@ -77,8 +105,9 @@ class SmartMonitor:
try:
result = subprocess.run(
["which", "smartctl"],
capture_output=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
check=False
)
return result.returncode == 0
@@ -99,8 +128,9 @@ class SmartMonitor:
# 获取基本信息
result = subprocess.run(
["sudo", "smartctl", "-a", device_path],
capture_output=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
check=False,
timeout=30
)
@@ -232,8 +262,9 @@ class SmartMonitor:
# 获取所有块设备
result = subprocess.run(
["lsblk", "-d", "-n", "-o", "NAME,TYPE,ROTA"],
capture_output=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
check=True
)
@@ -270,7 +301,7 @@ class SmartMonitor:
score = 70
# 根据错误数量调整
error_count = len(smart_info.errors)
error_count = len(smart_info.errors) if smart_info.errors else 0
score -= error_count * 10
# 确保分数在合理范围内

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

View File

@@ -4,6 +4,7 @@ import json
import logging
import re
import os
from system_compat import get_system_compat
logger = logging.getLogger(__name__)
@@ -26,20 +27,26 @@ class SystemInfoManager:
logger.debug(f"运行命令: {' '.join(command_list)}")
try:
# Python 3.6 兼容: 使用 stdout/stderr 参数代替 capture_output
# Python 3.6 兼容: 不使用 text 参数,手动解码
result = subprocess.run(
command_list,
capture_output=check_output,
text=True,
stdout=subprocess.PIPE if check_output else None,
stderr=subprocess.PIPE if check_output else None,
stdin=subprocess.PIPE if input_data else None,
check=True,
encoding='utf-8',
encoding='utf-8' if check_output else None,
input=input_data
)
return result.stdout if check_output else "", result.stderr if check_output else ""
if check_output:
return result.stdout, result.stderr
else:
return "", ""
except subprocess.CalledProcessError as e:
logger.error(f"命令执行失败: {' '.join(command_list)}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip()}")
logger.error(f"标准错误: {e.stderr.strip()}")
logger.error(f"标准输出: {e.stdout.strip() if e.stdout else ''}")
logger.error(f"标准错误: {e.stderr.strip() if e.stderr else ''}")
raise
except FileNotFoundError:
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
@@ -52,33 +59,32 @@ class SystemInfoManager:
"""
使用 lsblk 获取块设备信息。
返回一个字典列表,每个字典代表一个设备。
自动适配不同系统的 lsblk 版本。
"""
cmd = [
"lsblk", "-J", "-o",
"NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH"
]
try:
# 使用兼容性模块获取适当的命令
compat = get_system_compat()
cmd, need_manual_parse = compat.get_lsblk_command()
stdout, _ = self._run_command(cmd)
data = json.loads(stdout)
devices = data.get('blockdevices', [])
def add_path_recursive(dev_list):
devices = compat.parse_lsblk_output(stdout, need_manual_parse)
# 统一处理 path 字段
def normalize_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' not in dev and 'name' in dev:
dev['path'] = f"/dev/{dev['name']}"
if 'PATH' in dev:
dev['path'] = dev.pop('PATH')
if 'children' in dev:
add_path_recursive(dev['children'])
add_path_recursive(devices)
normalize_path_recursive(dev['children'])
normalize_path_recursive(devices)
return devices
except subprocess.CalledProcessError as e:
logger.error(f"获取块设备信息失败: {e.stderr.strip()}")
return []
except json.JSONDecodeError as e:
logger.error(f"解析 lsblk JSON 输出失败: {e}")
return []
except Exception as e:
logger.error(f"获取块设备信息失败 (未知错误): {e}")
return []
@@ -419,85 +425,50 @@ class SystemInfoManager:
def get_lvm_info(self):
"""
获取 LVM 物理卷 (PVs)、卷组 (VGs) 和逻辑卷 (LVs) 的信息。
自动适配不同系统的 LVM 版本。
"""
lvm_info = {'pvs': [], 'vgs': [], 'lvs': []}
# 使用兼容性模块获取适当的命令
compat = get_system_compat()
commands = compat.get_lvm_commands()
# Get PVs
try:
stdout, stderr = 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')
})
cmd, need_manual_parse = commands['pvs']
stdout, stderr = self._run_command(cmd)
lvm_info['pvs'] = compat.parse_pvs_output(stdout, need_manual_parse)
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.stderr.strip()}")
except json.JSONDecodeError as e:
logger.error(f"解析LVM物理卷JSON输出失败: {e}")
except Exception as e:
logger.error(f"获取LVM物理卷信息失败 (未知错误): {e}")
# Get VGs
try:
stdout, stderr = 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')
})
cmd, need_manual_parse = commands['vgs']
stdout, stderr = self._run_command(cmd)
lvm_info['vgs'] = compat.parse_vgs_output(stdout, need_manual_parse)
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.stderr.strip()}")
except json.JSONDecodeError as e:
logger.error(f"解析LVM卷组JSON输出失败: {e}")
except Exception as e:
logger.error(f"获取LVM卷组信息失败 (未知错误): {e}")
# Get LVs (MODIFIED: added -o lv_path)
# Get LVs
try:
# 明确请求 lv_path因为默认的 --reportformat json 不包含它
stdout, stderr = 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', []):
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') # 现在应该能正确获取到路径了
})
cmd, need_manual_parse = commands['lvs']
stdout, stderr = self._run_command(cmd)
lvm_info['lvs'] = compat.parse_lvs_output(stdout, need_manual_parse)
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.stderr.strip()}")
except json.JSONDecodeError as e:
logger.error(f"解析LVM逻辑卷JSON输出失败: {e}")
except Exception as e:
logger.error(f"获取LVM逻辑卷信息失败 (未知错误): {e}")