fix bug
This commit is contained in:
76
AGENTS.md
76
AGENTS.md
@@ -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
201
COMPAT_SUMMARY.md
Normal 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
215
README_COMPAT.md
Normal 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
216
build-compat.sh
Executable 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
29
build-simple.sh
Executable 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
292
dependency_checker.py
Normal 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(" 所有必需依赖已安装")
|
||||
@@ -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,
|
||||
ttk.Label(size_input_frame, text="大小 (GB):").pack(side=tk.LEFT)
|
||||
|
||||
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=max_gb,
|
||||
orient=tk.HORIZONTAL,
|
||||
variable=self.size_var,
|
||||
command=self._on_slider_change
|
||||
)
|
||||
self.size_slider.pack(fill=tk.X, pady=5)
|
||||
|
||||
# 输入框和标签
|
||||
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,
|
||||
to=available_gb,
|
||||
textvariable=self.size_var,
|
||||
width=10,
|
||||
command=self._on_spin_change
|
||||
)
|
||||
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():
|
||||
# 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:
|
||||
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()
|
||||
|
||||
|
||||
@@ -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
72
disk-manager-compat.spec
Normal 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,
|
||||
)
|
||||
@@ -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():
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['mainwindow_tkinter.py'],
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['main.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'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
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,
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='linux-storage-manager',
|
||||
@@ -31,8 +35,6 @@ exe = EXE(
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
entitlements_file=None )
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
131
main.py
Executable 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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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 表示没有进程被终止
|
||||
|
||||
@@ -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
337
platform_compat.py
Executable 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
19
run.sh
Executable 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
|
||||
@@ -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
364
system_compat.py
Executable file
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
系统命令兼容性模块
|
||||
处理不同 Linux 发行版之间系统命令的差异
|
||||
支持 Arch Linux 和 CentOS/RHEL 8
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
from platform_compat import get_platform_compat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemCommandCompat:
|
||||
"""系统命令兼容性处理类"""
|
||||
|
||||
def __init__(self):
|
||||
self._lsblk_version = None
|
||||
self._lvm_version = None
|
||||
self._parted_version = None
|
||||
self._compat_cache = {}
|
||||
|
||||
def _get_lsblk_version(self):
|
||||
"""获取 lsblk 版本"""
|
||||
if self._lsblk_version is None:
|
||||
try:
|
||||
# Python 3.6 兼容
|
||||
result = subprocess.run(
|
||||
['lsblk', '--version'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
|
||||
# 解析版本号,例如 "lsblk from util-linux 2.37.4"
|
||||
match = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', output)
|
||||
if match:
|
||||
self._lsblk_version = tuple(int(x) if x else 0 for x in match.groups())
|
||||
logger.debug(f"检测到 lsblk 版本: {self._lsblk_version}")
|
||||
else:
|
||||
self._lsblk_version = (0, 0, 0)
|
||||
except Exception as e:
|
||||
logger.warning(f"无法检测 lsblk 版本: {e}")
|
||||
self._lsblk_version = (0, 0, 0)
|
||||
return self._lsblk_version
|
||||
|
||||
def _get_lvm_version(self):
|
||||
"""获取 LVM 版本"""
|
||||
if self._lvm_version is None:
|
||||
try:
|
||||
# Python 3.6 兼容
|
||||
result = subprocess.run(
|
||||
['lvm', 'version'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
output = result.stdout.decode('utf-8', errors='ignore') + result.stderr.decode('utf-8', errors='ignore')
|
||||
# 解析版本号,例如 "LVM version: 2.03.14"
|
||||
match = re.search(r'(\d+)\.(\d+)\.(\d+)', output)
|
||||
if match:
|
||||
self._lvm_version = tuple(int(x) for x in match.groups())
|
||||
logger.debug(f"检测到 LVM 版本: {self._lvm_version}")
|
||||
else:
|
||||
self._lvm_version = (0, 0, 0)
|
||||
except Exception as e:
|
||||
logger.warning(f"无法检测 LVM 版本: {e}")
|
||||
self._lvm_version = (0, 0, 0)
|
||||
return self._lvm_version
|
||||
|
||||
def supports_lsblk_json(self):
|
||||
"""检查 lsblk 是否支持 JSON 输出 (util-linux 2.23+)"""
|
||||
version = self._get_lsblk_version()
|
||||
# util-linux 2.23 引入 JSON 支持
|
||||
return version >= (2, 23)
|
||||
|
||||
def supports_lvm_json(self):
|
||||
"""检查 LVM 是否支持 JSON 报告格式 (2.02.166+)"""
|
||||
version = self._get_lvm_version()
|
||||
# LVM 2.02.166 引入 JSON 报告格式
|
||||
return version >= (2, 2, 166) or version >= (2, 3, 0)
|
||||
|
||||
def get_lsblk_command(self):
|
||||
"""
|
||||
获取适用于当前系统的 lsblk 命令
|
||||
返回: (命令列表, 是否需要手动解析)
|
||||
"""
|
||||
# 注意: PATH 列在较新的 lsblk 中可用,但在 CentOS 8 等旧系统中不可用
|
||||
# 使用 KNAME 代替,它是内部内核设备名称,总是存在
|
||||
if self.supports_lsblk_json():
|
||||
# 现代系统支持 JSON
|
||||
return [
|
||||
"lsblk", "-J", "-o",
|
||||
"NAME,KNAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME"
|
||||
], False
|
||||
else:
|
||||
# 旧系统回退到普通输出
|
||||
logger.warning("lsblk 不支持 JSON 输出,使用回退方案")
|
||||
return [
|
||||
"lsblk", "-o",
|
||||
"NAME,KNAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME",
|
||||
"-n", "-p" # -n 不打印标题,-p 显示完整路径
|
||||
], True
|
||||
|
||||
def parse_lsblk_output(self, stdout, use_manual_parse=False):
|
||||
"""
|
||||
解析 lsblk 输出
|
||||
:param stdout: 命令输出
|
||||
:param use_manual_parse: 是否需要手动解析(非 JSON)
|
||||
:return: 设备列表
|
||||
"""
|
||||
if not use_manual_parse:
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
devices = data.get('blockdevices', [])
|
||||
|
||||
# 统一处理 PATH 字段
|
||||
# 注意: 使用 kname (内核设备名) 来生成 path,因为 PATH 列在某些系统中不可用
|
||||
def normalize_device(dev):
|
||||
# 优先使用 kname (内核设备名),因为它总是存在且是规范的
|
||||
if 'kname' in dev:
|
||||
dev['path'] = f"/dev/{dev['kname']}"
|
||||
elif 'path' not in dev and 'name' in dev:
|
||||
dev['path'] = f"/dev/{dev['name']}"
|
||||
# 清理旧字段
|
||||
if 'PATH' in dev:
|
||||
dev.pop('PATH')
|
||||
if 'KNAME' in dev:
|
||||
dev['kname'] = dev.pop('KNAME')
|
||||
if 'children' in dev:
|
||||
for child in dev['children']:
|
||||
normalize_device(child)
|
||||
return dev
|
||||
|
||||
for dev in devices:
|
||||
normalize_device(dev)
|
||||
|
||||
return devices
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON 解析失败,尝试手动解析: {e}")
|
||||
use_manual_parse = True
|
||||
|
||||
if use_manual_parse:
|
||||
return self._manual_parse_lsblk(stdout)
|
||||
|
||||
return []
|
||||
|
||||
def _manual_parse_lsblk(self, stdout):
|
||||
"""
|
||||
手动解析 lsblk 的普通输出
|
||||
用于不支持 JSON 的旧系统
|
||||
"""
|
||||
devices = []
|
||||
device_stack = [] # 用于处理层次结构
|
||||
|
||||
lines = stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# 解析缩进级别(每两个空格或一个制表符表示一个层级)
|
||||
indent_level = 0
|
||||
stripped_line = line
|
||||
while stripped_line.startswith(' ') or stripped_line.startswith('├') or stripped_line.startswith('└') or stripped_line.startswith('│'):
|
||||
if stripped_line[0] in '├└│':
|
||||
indent_level += 1
|
||||
stripped_line = stripped_line[1:]
|
||||
if stripped_line.startswith('─'):
|
||||
stripped_line = stripped_line[1:]
|
||||
if stripped_line.startswith(' '):
|
||||
stripped_line = stripped_line[1:]
|
||||
|
||||
# 分割字段
|
||||
parts = stripped_line.split()
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
|
||||
device = {
|
||||
'name': parts[0],
|
||||
'path': f"/dev/{parts[0]}" if not parts[0].startswith('/') else parts[0],
|
||||
'fstype': parts[1] if parts[1] != '' else None,
|
||||
'size': parts[2],
|
||||
'mountpoint': parts[3] if parts[3] != '' else None,
|
||||
'ro': parts[4] == '1',
|
||||
'type': parts[5],
|
||||
}
|
||||
|
||||
# 添加可选字段
|
||||
if len(parts) > 6:
|
||||
device['uuid'] = parts[6] if parts[6] != '' else None
|
||||
if len(parts) > 7:
|
||||
device['partuuid'] = parts[7] if parts[7] != '' else None
|
||||
|
||||
# 处理层次结构
|
||||
if indent_level == 0:
|
||||
devices.append(device)
|
||||
device_stack = [device]
|
||||
else:
|
||||
# 找到正确的父设备
|
||||
while len(device_stack) > indent_level:
|
||||
device_stack.pop()
|
||||
|
||||
if device_stack:
|
||||
parent = device_stack[-1]
|
||||
if 'children' not in parent:
|
||||
parent['children'] = []
|
||||
parent['children'].append(device)
|
||||
device_stack.append(device)
|
||||
else:
|
||||
devices.append(device)
|
||||
device_stack = [device]
|
||||
|
||||
return devices
|
||||
|
||||
def get_lvm_commands(self):
|
||||
"""
|
||||
获取适用于当前系统的 LVM 命令
|
||||
返回: {
|
||||
'pvs': (命令列表, 是否需要手动解析),
|
||||
'vgs': (命令列表, 是否需要手动解析),
|
||||
'lvs': (命令列表, 是否需要手动解析)
|
||||
}
|
||||
"""
|
||||
if self.supports_lvm_json():
|
||||
return {
|
||||
'pvs': (['pvs', '--reportformat', 'json'], False),
|
||||
'vgs': (['vgs', '--reportformat', 'json'], False),
|
||||
'lvs': (['lvs', '-o', 'lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path', '--reportformat', 'json'], False),
|
||||
}
|
||||
else:
|
||||
logger.warning("LVM 不支持 JSON 输出,使用回退方案")
|
||||
return {
|
||||
'pvs': (['pvs', '--noheadings', '--separator', '|', '-o', 'pv_name,vg_name,pv_uuid,pv_size,pv_free,pv_attr,pv_fmt'], True),
|
||||
'vgs': (['vgs', '--noheadings', '--separator', '|', '-o', 'vg_name,vg_uuid,vg_size,vg_free,vg_attr,pv_count,lv_count,vg_alloc_percent,vg_fmt'], True),
|
||||
'lvs': (['lvs', '--noheadings', '--separator', '|', '-o', 'lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path'], True),
|
||||
}
|
||||
|
||||
def parse_pvs_output(self, stdout, use_manual_parse=False):
|
||||
"""解析 pvs 输出"""
|
||||
if not use_manual_parse:
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
return [{
|
||||
'pv_name': pv.get('pv_name'),
|
||||
'vg_name': pv.get('vg_name'),
|
||||
'pv_uuid': pv.get('pv_uuid'),
|
||||
'pv_size': pv.get('pv_size'),
|
||||
'pv_free': pv.get('pv_free'),
|
||||
'pv_attr': pv.get('pv_attr'),
|
||||
'pv_fmt': pv.get('pv_fmt')
|
||||
} for pv in data['report'][0].get('pv', [])]
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
use_manual_parse = True
|
||||
|
||||
if use_manual_parse:
|
||||
pvs = []
|
||||
for line in stdout.strip().split('\n'):
|
||||
parts = line.strip().split('|')
|
||||
if len(parts) >= 7:
|
||||
pvs.append({
|
||||
'pv_name': parts[0].strip(),
|
||||
'vg_name': parts[1].strip() if parts[1].strip() else None,
|
||||
'pv_uuid': parts[2].strip() if len(parts) > 2 else None,
|
||||
'pv_size': parts[3].strip() if len(parts) > 3 else None,
|
||||
'pv_free': parts[4].strip() if len(parts) > 4 else None,
|
||||
'pv_attr': parts[5].strip() if len(parts) > 5 else None,
|
||||
'pv_fmt': parts[6].strip() if len(parts) > 6 else None,
|
||||
})
|
||||
return pvs
|
||||
return []
|
||||
|
||||
def parse_vgs_output(self, stdout, use_manual_parse=False):
|
||||
"""解析 vgs 输出"""
|
||||
if not use_manual_parse:
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
return [{
|
||||
'vg_name': vg.get('vg_name'),
|
||||
'vg_uuid': vg.get('vg_uuid'),
|
||||
'vg_size': vg.get('vg_size'),
|
||||
'vg_free': vg.get('vg_free'),
|
||||
'vg_attr': vg.get('vg_attr'),
|
||||
'pv_count': vg.get('pv_count'),
|
||||
'lv_count': vg.get('lv_count'),
|
||||
'vg_alloc_percent': vg.get('vg_alloc_percent'),
|
||||
'vg_fmt': vg.get('vg_fmt')
|
||||
} for vg in data['report'][0].get('vg', [])]
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
use_manual_parse = True
|
||||
|
||||
if use_manual_parse:
|
||||
vgs = []
|
||||
for line in stdout.strip().split('\n'):
|
||||
parts = line.strip().split('|')
|
||||
if len(parts) >= 9:
|
||||
vgs.append({
|
||||
'vg_name': parts[0].strip(),
|
||||
'vg_uuid': parts[1].strip() if len(parts) > 1 else None,
|
||||
'vg_size': parts[2].strip() if len(parts) > 2 else None,
|
||||
'vg_free': parts[3].strip() if len(parts) > 3 else None,
|
||||
'vg_attr': parts[4].strip() if len(parts) > 4 else None,
|
||||
'pv_count': parts[5].strip() if len(parts) > 5 else None,
|
||||
'lv_count': parts[6].strip() if len(parts) > 6 else None,
|
||||
'vg_alloc_percent': parts[7].strip() if len(parts) > 7 else None,
|
||||
'vg_fmt': parts[8].strip() if len(parts) > 8 else None,
|
||||
})
|
||||
return vgs
|
||||
return []
|
||||
|
||||
def parse_lvs_output(self, stdout, use_manual_parse=False):
|
||||
"""解析 lvs 输出"""
|
||||
if not use_manual_parse:
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
if 'report' in data and data['report']:
|
||||
return [{
|
||||
'lv_name': lv.get('lv_name'),
|
||||
'vg_name': lv.get('vg_name'),
|
||||
'lv_uuid': lv.get('lv_uuid'),
|
||||
'lv_size': lv.get('lv_size'),
|
||||
'lv_attr': lv.get('lv_attr'),
|
||||
'origin': lv.get('origin'),
|
||||
'snap_percent': lv.get('snap_percent'),
|
||||
'lv_path': lv.get('lv_path')
|
||||
} for lv in data['report'][0].get('lv', [])]
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
use_manual_parse = True
|
||||
|
||||
if use_manual_parse:
|
||||
lvs = []
|
||||
for line in stdout.strip().split('\n'):
|
||||
parts = line.strip().split('|')
|
||||
if len(parts) >= 8:
|
||||
lvs.append({
|
||||
'lv_name': parts[0].strip(),
|
||||
'vg_name': parts[1].strip(),
|
||||
'lv_uuid': parts[2].strip() if len(parts) > 2 else None,
|
||||
'lv_size': parts[3].strip() if len(parts) > 3 else None,
|
||||
'lv_attr': parts[4].strip() if len(parts) > 4 else None,
|
||||
'origin': parts[5].strip() if len(parts) > 5 else None,
|
||||
'snap_percent': parts[6].strip() if len(parts) > 6 else None,
|
||||
'lv_path': parts[7].strip() if len(parts) > 7 else None,
|
||||
})
|
||||
return lvs
|
||||
return []
|
||||
|
||||
|
||||
# 全局实例
|
||||
_compat_instance = None
|
||||
|
||||
def get_system_compat():
|
||||
"""获取全局系统兼容性实例"""
|
||||
global _compat_instance
|
||||
if _compat_instance is None:
|
||||
_compat_instance = SystemCommandCompat()
|
||||
return _compat_instance
|
||||
115
system_info.py
115
system_info.py
@@ -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:
|
||||
stdout, _ = self._run_command(cmd)
|
||||
data = json.loads(stdout)
|
||||
devices = data.get('blockdevices', [])
|
||||
# 使用兼容性模块获取适当的命令
|
||||
compat = get_system_compat()
|
||||
cmd, need_manual_parse = compat.get_lsblk_command()
|
||||
|
||||
def add_path_recursive(dev_list):
|
||||
stdout, _ = self._run_command(cmd)
|
||||
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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user