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`
|
2. 检查系统命令是否安装:`which parted mdadm lvm`
|
||||||
3. 手动测试命令:`sudo lsblk -J`
|
3. 手动测试命令:`sudo lsblk -J`
|
||||||
4. 检查权限:确保用户有 sudo 权限或已切换到 root
|
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__)
|
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:
|
class EnhancedFormatDialog:
|
||||||
"""增强格式化对话框 - 显示详细警告信息"""
|
"""增强格式化对话框 - 显示详细警告信息"""
|
||||||
|
|
||||||
@@ -19,7 +250,7 @@ class EnhancedFormatDialog:
|
|||||||
|
|
||||||
self.dialog = tk.Toplevel(parent)
|
self.dialog = tk.Toplevel(parent)
|
||||||
self.dialog.title(f"格式化确认 - {device_path}")
|
self.dialog.title(f"格式化确认 - {device_path}")
|
||||||
self.dialog.geometry("500x575")
|
self.dialog.geometry("500x500")
|
||||||
self.dialog.minsize(480, 450)
|
self.dialog.minsize(480, 450)
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
self.dialog.grab_set()
|
||||||
@@ -28,14 +259,17 @@ class EnhancedFormatDialog:
|
|||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
|
"""居中窗口"""
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
width = self.dialog.winfo_width()
|
width = 500
|
||||||
height = self.dialog.winfo_height()
|
height = 500
|
||||||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||||||
y = (self.dialog.winfo_screenheight() // 2) - (height // 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):
|
def _create_widgets(self):
|
||||||
|
"""创建界面元素"""
|
||||||
|
# 主框架
|
||||||
main_frame = ttk.Frame(self.dialog, padding="15")
|
main_frame = ttk.Frame(self.dialog, padding="15")
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
@@ -43,58 +277,45 @@ class EnhancedFormatDialog:
|
|||||||
warning_frame = ttk.Frame(main_frame)
|
warning_frame = ttk.Frame(main_frame)
|
||||||
warning_frame.pack(fill=tk.X, pady=(0, 15))
|
warning_frame.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
warning_label = ttk.Label(
|
ttk.Label(warning_frame, text="⚠️ 警告",
|
||||||
warning_frame,
|
font=('Arial', 16, 'bold'),
|
||||||
text="⚠ 警告:此操作将永久删除所有数据!",
|
foreground='red').pack(anchor=tk.W)
|
||||||
font=("Arial", 12, "bold"),
|
|
||||||
foreground="red"
|
|
||||||
)
|
|
||||||
warning_label.pack()
|
|
||||||
|
|
||||||
# 设备信息区域
|
# 设备信息
|
||||||
info_frame = ttk.LabelFrame(main_frame, text="设备信息", padding="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, 15))
|
||||||
|
|
||||||
# 显示详细信息
|
ttk.Label(info_frame,
|
||||||
infos = [
|
text=f"设备路径: {self.device_path}",
|
||||||
("设备路径", self.device_path),
|
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=2)
|
||||||
("设备名称", self.device_info.get('name', 'N/A')),
|
ttk.Label(info_frame,
|
||||||
("文件系统", self.device_info.get('fstype', 'N/A')),
|
text=f"设备类型: {self.device_info.get('type', 'Unknown')}",
|
||||||
("大小", self.device_info.get('size', 'N/A')),
|
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
|
||||||
("UUID", self.device_info.get('uuid', 'N/A')),
|
|
||||||
("挂载点", self.device_info.get('mountpoint', '未挂载')),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, (label, value) in enumerate(infos):
|
# 显示文件系统信息
|
||||||
ttk.Label(info_frame, text=f"{label}:", font=("Arial", 10, "bold")).grid(
|
fstype = self.device_info.get('fstype', '')
|
||||||
row=i, column=0, sticky=tk.W, padx=5, pady=3
|
if fstype:
|
||||||
)
|
ttk.Label(info_frame,
|
||||||
val_label = ttk.Label(info_frame, text=str(value))
|
text=f"当前文件系统: {fstype}",
|
||||||
val_label.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
|
||||||
|
|
||||||
# 如果有挂载点,高亮显示
|
size = self.device_info.get('size', 'Unknown')
|
||||||
if label == "挂载点" and value and value != '未挂载':
|
ttk.Label(info_frame,
|
||||||
val_label.configure(foreground="orange", font=("Arial", 9, "bold"))
|
text=f"设备大小: {size}",
|
||||||
|
font=('Arial', 9)).pack(anchor=tk.W, pady=2)
|
||||||
|
|
||||||
# 数据风险提示
|
# 危险警告
|
||||||
risk_frame = ttk.LabelFrame(main_frame, text="数据风险提示", padding="10")
|
danger_frame = ttk.Frame(main_frame)
|
||||||
risk_frame.pack(fill=tk.X, pady=(0, 15))
|
danger_frame.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
risks = []
|
ttk.Label(danger_frame,
|
||||||
if self.device_info.get('fstype'):
|
text="此操作将永久删除设备上的所有数据!",
|
||||||
risks.append(f"• 此分区包含 {self.device_info['fstype']} 文件系统")
|
font=('Arial', 10, 'bold'),
|
||||||
if self.device_info.get('uuid'):
|
foreground='red').pack(anchor=tk.W, pady=5)
|
||||||
risks.append(f"• 分区有 UUID: {self.device_info['uuid'][:20]}...")
|
|
||||||
if self.device_info.get('mountpoint'):
|
|
||||||
risks.append(f"• 分区当前挂载在: {self.device_info['mountpoint']}")
|
|
||||||
|
|
||||||
if not risks:
|
ttk.Label(danger_frame,
|
||||||
risks.append("• 无法获取分区详细信息")
|
text="• 所有数据将无法恢复\n• 请确保已备份重要数据",
|
||||||
|
foreground='red').pack(anchor=tk.W, pady=5)
|
||||||
for risk in risks:
|
|
||||||
ttk.Label(risk_frame, text=risk, foreground="red").pack(
|
|
||||||
anchor=tk.W, pady=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# 文件系统选择
|
# 文件系统选择
|
||||||
fs_frame = ttk.LabelFrame(main_frame, text="选择新文件系统", padding="10")
|
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,
|
ttk.Radiobutton(fs_frame, text=fs, variable=self.fs_var,
|
||||||
value=fs).pack(side=tk.LEFT, padx=10)
|
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 = ttk.Frame(main_frame)
|
||||||
btn_frame.pack(fill=tk.X, pady=10)
|
btn_frame.pack(fill=tk.X, pady=10)
|
||||||
@@ -130,12 +338,6 @@ class EnhancedFormatDialog:
|
|||||||
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
|
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
def _on_confirm(self):
|
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.result = self.fs_var.get()
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
@@ -158,9 +360,9 @@ class EnhancedRaidDeleteDialog:
|
|||||||
self.result = False
|
self.result = False
|
||||||
|
|
||||||
self.dialog = tk.Toplevel(parent)
|
self.dialog = tk.Toplevel(parent)
|
||||||
self.dialog.title(f"删除 RAID 阵列确认 - {array_path}")
|
self.dialog.title(f"删除 RAID 阵列 - {array_path}")
|
||||||
self.dialog.geometry("550x750")
|
self.dialog.geometry("500x500")
|
||||||
self.dialog.minsize(520, 750)
|
self.dialog.minsize(450, 400)
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
self.dialog.grab_set()
|
||||||
|
|
||||||
@@ -168,96 +370,62 @@ class EnhancedRaidDeleteDialog:
|
|||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
|
"""居中窗口"""
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
width = self.dialog.winfo_width()
|
width = 500
|
||||||
height = self.dialog.winfo_height()
|
height = 500
|
||||||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||||||
y = (self.dialog.winfo_screenheight() // 2) - (height // 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):
|
def _create_widgets(self):
|
||||||
|
"""创建界面元素"""
|
||||||
|
# 主框架
|
||||||
main_frame = ttk.Frame(self.dialog, padding="15")
|
main_frame = ttk.Frame(self.dialog, padding="15")
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# 危险警告
|
# 警告标题
|
||||||
warning_frame = ttk.Frame(main_frame)
|
ttk.Label(main_frame, text="⚠️ 确认删除 RAID 阵列",
|
||||||
warning_frame.pack(fill=tk.X, pady=(0, 15))
|
font=('Arial', 14, 'bold'),
|
||||||
|
foreground='red').pack(anchor=tk.W, pady=(0, 10))
|
||||||
|
|
||||||
warning_label = ttk.Label(
|
# 阵列信息
|
||||||
warning_frame,
|
info_frame = ttk.LabelFrame(main_frame, text="阵列信息", padding="10")
|
||||||
text="⚠ 危险操作:此操作将永久删除 RAID 阵列!",
|
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
font=("Arial", 13, "bold"),
|
|
||||||
foreground="red"
|
|
||||||
)
|
|
||||||
warning_label.pack()
|
|
||||||
|
|
||||||
# 阵列详情
|
ttk.Label(info_frame, text=f"阵列名称: {self.array_path}",
|
||||||
array_frame = ttk.LabelFrame(main_frame, text="阵列详情", padding="10")
|
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=2)
|
||||||
array_frame.pack(fill=tk.X, pady=(0, 15))
|
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)
|
||||||
|
|
||||||
details = [
|
# 成员设备
|
||||||
("阵列设备", self.array_path),
|
member_frame = ttk.LabelFrame(main_frame, text="成员设备", padding="10")
|
||||||
("RAID 级别", self.array_data.get('level', 'N/A')),
|
member_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
("阵列状态", 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')),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, (label, value) in enumerate(details):
|
for dev in self.member_devices:
|
||||||
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}",
|
ttk.Label(member_frame, text=f"• {dev}",
|
||||||
font=("Arial", 10), foreground="red").pack(
|
font=('Arial', 9)).pack(anchor=tk.W, pady=1)
|
||||||
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")
|
danger_frame = ttk.Frame(main_frame)
|
||||||
consequence_frame.pack(fill=tk.X, pady=(0, 15))
|
danger_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
consequences = [
|
ttk.Label(danger_frame,
|
||||||
"• 阵列将被停止并从系统中移除",
|
text="⚠️ 删除操作将:",
|
||||||
"• 所有成员设备的 RAID 超级块将被清除",
|
font=('Arial', 10, 'bold'),
|
||||||
"• 阵列上的所有数据将永久丢失",
|
foreground='red').pack(anchor=tk.W, pady=5)
|
||||||
"• /etc/mdadm.conf 中的配置将被删除",
|
|
||||||
"• 此操作无法撤销!"
|
warnings = [
|
||||||
|
"• 停止并删除 RAID 阵列",
|
||||||
|
"• 清除成员设备上的 RAID 超级块",
|
||||||
|
"• 从配置文件中移除阵列配置",
|
||||||
|
"• 所有数据将永久丢失且无法恢复"
|
||||||
]
|
]
|
||||||
|
for warning in warnings:
|
||||||
for con in consequences:
|
ttk.Label(danger_frame, text=warning,
|
||||||
ttk.Label(consequence_frame, text=con,
|
foreground='red').pack(anchor=tk.W, pady=2)
|
||||||
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()
|
|
||||||
|
|
||||||
# 按钮
|
# 按钮
|
||||||
btn_frame = ttk.Frame(main_frame)
|
btn_frame = ttk.Frame(main_frame)
|
||||||
@@ -266,23 +434,14 @@ class EnhancedRaidDeleteDialog:
|
|||||||
ttk.Button(btn_frame, text="取消",
|
ttk.Button(btn_frame, text="取消",
|
||||||
command=self._on_cancel).pack(side=tk.RIGHT, padx=5)
|
command=self._on_cancel).pack(side=tk.RIGHT, padx=5)
|
||||||
ttk.Button(btn_frame, text="确认删除",
|
ttk.Button(btn_frame, text="确认删除",
|
||||||
command=self._on_confirm,
|
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
|
||||||
style="Danger.TButton").pack(side=tk.RIGHT, padx=5)
|
|
||||||
|
|
||||||
# 配置危险按钮样式
|
|
||||||
style = ttk.Style()
|
|
||||||
style.configure("Danger.TButton", foreground="red")
|
|
||||||
|
|
||||||
def _on_confirm(self):
|
def _on_confirm(self):
|
||||||
if self.confirm_var.get() != "DELETE":
|
|
||||||
messagebox.showerror("错误",
|
|
||||||
"输入错误!请输入 'DELETE'(全大写)以确认删除操作。")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.result = True
|
self.result = True
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
def _on_cancel(self):
|
def _on_cancel(self):
|
||||||
|
self.result = False
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
def wait_for_result(self):
|
def wait_for_result(self):
|
||||||
@@ -291,7 +450,7 @@ class EnhancedRaidDeleteDialog:
|
|||||||
|
|
||||||
|
|
||||||
class EnhancedPartitionDialog:
|
class EnhancedPartitionDialog:
|
||||||
"""增强分区创建对话框 - 可视化滑块选择大小"""
|
"""增强分区创建对话框 - 智能分区建议"""
|
||||||
|
|
||||||
def __init__(self, parent, disk_path, total_disk_mib, max_available_mib):
|
def __init__(self, parent, disk_path, total_disk_mib, max_available_mib):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
@@ -302,8 +461,8 @@ class EnhancedPartitionDialog:
|
|||||||
|
|
||||||
self.dialog = tk.Toplevel(parent)
|
self.dialog = tk.Toplevel(parent)
|
||||||
self.dialog.title(f"创建分区 - {disk_path}")
|
self.dialog.title(f"创建分区 - {disk_path}")
|
||||||
self.dialog.geometry("550x575")
|
self.dialog.geometry("550x550")
|
||||||
self.dialog.minsize(500, 575)
|
self.dialog.minsize(500, 500)
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
self.dialog.grab_set()
|
||||||
|
|
||||||
@@ -311,90 +470,99 @@ class EnhancedPartitionDialog:
|
|||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
|
"""居中窗口"""
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
width = self.dialog.winfo_width()
|
width = 550
|
||||||
height = self.dialog.winfo_height()
|
height = 550
|
||||||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||||||
y = (self.dialog.winfo_screenheight() // 2) - (height // 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):
|
def _create_widgets(self):
|
||||||
|
"""创建界面元素"""
|
||||||
|
# 主框架
|
||||||
main_frame = ttk.Frame(self.dialog, padding="15")
|
main_frame = ttk.Frame(self.dialog, padding="15")
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
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 = 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
|
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,
|
||||||
ttk.Label(info_frame, text=f"总容量: {total_gb:.2f} GB").pack(anchor=tk.W, pady=2)
|
text=f"磁盘总大小: {total_gb:.2f} GB").pack(anchor=tk.W)
|
||||||
ttk.Label(info_frame, text=f"可用空间: {max_gb:.2f} GB",
|
ttk.Label(info_frame,
|
||||||
foreground="green").pack(anchor=tk.W, pady=2)
|
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")
|
part_type_frame = ttk.LabelFrame(main_frame, text="分区表类型", padding="10")
|
||||||
pt_frame.pack(fill=tk.X, pady=(0, 15))
|
part_type_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
self.part_type_var = tk.StringVar(value="gpt")
|
self.part_type_var = tk.StringVar(value="gpt")
|
||||||
ttk.Radiobutton(pt_frame, text="GPT (推荐,支持大容量磁盘)",
|
ttk.Radiobutton(part_type_frame, text="GPT (推荐,支持2TB以上磁盘)",
|
||||||
variable=self.part_type_var, value="gpt").pack(anchor=tk.W, pady=2)
|
variable=self.part_type_var,
|
||||||
ttk.Radiobutton(pt_frame, text="MBR/MSDOS (兼容旧系统)",
|
value="gpt").pack(anchor=tk.W, pady=2)
|
||||||
variable=self.part_type_var, value="msdos").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 = 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)
|
self.use_max_var = tk.BooleanVar(value=False)
|
||||||
ttk.Checkbutton(size_frame, text="使用最大可用空间",
|
ttk.Checkbutton(size_frame,
|
||||||
|
text="使用全部可用空间",
|
||||||
variable=self.use_max_var,
|
variable=self.use_max_var,
|
||||||
command=self._toggle_size_input).pack(anchor=tk.W, pady=5)
|
command=self._toggle_size_input).pack(anchor=tk.W, pady=5)
|
||||||
|
|
||||||
# 滑块和输入框容器
|
# 大小输入
|
||||||
self.size_control_frame = ttk.Frame(size_frame)
|
size_input_frame = ttk.Frame(size_frame)
|
||||||
self.size_control_frame.pack(fill=tk.X, pady=5)
|
size_input_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
# 滑块
|
ttk.Label(size_input_frame, text="大小 (GB):").pack(side=tk.LEFT)
|
||||||
self.size_var = tk.DoubleVar(value=max_gb)
|
|
||||||
self.size_slider = ttk.Scale(
|
self.size_var = tk.DoubleVar(value=min(available_gb, 100))
|
||||||
self.size_control_frame,
|
# 使用 tk.Spinbox 兼容旧版 tkinter (Python 3.6)
|
||||||
|
try:
|
||||||
|
self.size_spin = tk.Spinbox(size_input_frame,
|
||||||
from_=0.1,
|
from_=0.1,
|
||||||
to=max_gb,
|
to=available_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,
|
|
||||||
textvariable=self.size_var,
|
textvariable=self.size_var,
|
||||||
width=10,
|
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)
|
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_var = tk.StringVar(value="0%")
|
||||||
self.percent_label.pack(side=tk.LEFT, padx=10)
|
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()
|
self._toggle_size_input()
|
||||||
@@ -405,90 +573,49 @@ class EnhancedPartitionDialog:
|
|||||||
|
|
||||||
ttk.Button(btn_frame, text="取消",
|
ttk.Button(btn_frame, text="取消",
|
||||||
command=self._on_cancel).pack(side=tk.RIGHT, padx=5)
|
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)
|
command=self._on_confirm).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
def _create_space_visualizer(self, parent, max_gb):
|
def _on_size_change(self, *args):
|
||||||
"""创建空间可视化条"""
|
"""大小输入变化时更新滑块"""
|
||||||
viz_frame = ttk.LabelFrame(parent, text="空间使用预览", padding="10")
|
try:
|
||||||
viz_frame.pack(fill=tk.X, pady=(0, 15))
|
value = self.size_var.get()
|
||||||
|
self.size_slider.set(value)
|
||||||
# Canvas 用于绘制
|
self._update_percent(value)
|
||||||
self.viz_canvas = tk.Canvas(viz_frame, height=40, bg="white")
|
except:
|
||||||
self.viz_canvas.pack(fill=tk.X, pady=5)
|
pass
|
||||||
|
|
||||||
# 等待 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_slider_change(self, value):
|
def _on_slider_change(self, value):
|
||||||
"""滑块变化回调"""
|
"""滑块变化时更新输入"""
|
||||||
try:
|
try:
|
||||||
gb = float(value)
|
self.size_var.set(float(value))
|
||||||
self._update_percent(gb)
|
self._update_percent(float(value))
|
||||||
self._update_visualizer(gb, self.max_available_mib / 1024)
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _on_spin_change(self):
|
def _update_percent(self, value):
|
||||||
"""输入框变化回调"""
|
|
||||||
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):
|
|
||||||
"""更新百分比显示"""
|
"""更新百分比显示"""
|
||||||
max_gb = self.max_available_mib / 1024
|
if self.max_available_mib > 0:
|
||||||
if max_gb > 0:
|
percent = (value * 1024 / self.max_available_mib) * 100
|
||||||
percent = (gb / max_gb) * 100
|
self.percent_var.set(f"{percent:.1f}%")
|
||||||
self.percent_label.configure(text=f"({percent:.1f}%)")
|
|
||||||
|
|
||||||
def _toggle_size_input(self):
|
def _toggle_size_input(self):
|
||||||
"""切换大小输入状态"""
|
"""切换大小输入状态"""
|
||||||
if self.use_max_var.get():
|
if self.use_max_var.get():
|
||||||
|
# ttk.Scale 在某些旧版本 Tkinter 中不支持 state 选项
|
||||||
|
try:
|
||||||
self.size_slider.configure(state=tk.DISABLED)
|
self.size_slider.configure(state=tk.DISABLED)
|
||||||
|
except tk.TclError:
|
||||||
|
pass # 忽略不支持的选项
|
||||||
self.size_spin.configure(state=tk.DISABLED)
|
self.size_spin.configure(state=tk.DISABLED)
|
||||||
max_gb = self.max_available_mib / 1024
|
max_gb = self.max_available_mib / 1024
|
||||||
self.size_var.set(max_gb)
|
self.size_var.set(max_gb)
|
||||||
self._update_percent(max_gb)
|
self._update_percent(max_gb)
|
||||||
self._update_visualizer(max_gb, max_gb)
|
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
self.size_slider.configure(state=tk.NORMAL)
|
self.size_slider.configure(state=tk.NORMAL)
|
||||||
|
except tk.TclError:
|
||||||
|
pass # 忽略不支持的选项
|
||||||
self.size_spin.configure(state=tk.NORMAL)
|
self.size_spin.configure(state=tk.NORMAL)
|
||||||
|
|
||||||
def _on_confirm(self):
|
def _on_confirm(self):
|
||||||
@@ -508,8 +635,8 @@ class EnhancedPartitionDialog:
|
|||||||
'disk_path': self.disk_path,
|
'disk_path': self.disk_path,
|
||||||
'partition_table_type': self.part_type_var.get(),
|
'partition_table_type': self.part_type_var.get(),
|
||||||
'size_gb': size_gb,
|
'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()
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ class SmartInfoDialog:
|
|||||||
self.dialog.title(f"SMART 信息 - {device_path}")
|
self.dialog.title(f"SMART 信息 - {device_path}")
|
||||||
self.dialog.geometry("700x600")
|
self.dialog.geometry("700x600")
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
|
||||||
|
|
||||||
self._center_window()
|
self._center_window()
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
|
# 窗口可见后再设置 grab_set
|
||||||
|
self.dialog.after(100, self.dialog.grab_set)
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
"""居中显示"""
|
"""居中显示"""
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
@@ -213,12 +215,14 @@ class SmartOverviewDialog:
|
|||||||
self.dialog.title("SMART 健康监控 - 总览")
|
self.dialog.title("SMART 健康监控 - 总览")
|
||||||
self.dialog.geometry("800x500")
|
self.dialog.geometry("800x500")
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
|
||||||
|
|
||||||
self._center_window()
|
self._center_window()
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
self._refresh_data()
|
self._refresh_data()
|
||||||
|
|
||||||
|
# 窗口可见后再设置 grab_set
|
||||||
|
self.dialog.after(100, self.dialog.grab_set)
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
"""居中显示"""
|
"""居中显示"""
|
||||||
self.dialog.update_idletasks()
|
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:
|
else:
|
||||||
messagebox.showerror("错误", f"挂载设备 {device_path} 失败。\n\n"
|
messagebox.showerror("错误", f"挂载设备 {device_path} 失败。\n\n"
|
||||||
f"普通挂载和 ntfs-3g 挂载均失败。\n"
|
f"普通挂载和 ntfs-3g 挂载均失败。\n"
|
||||||
f"如果是 NTFS 文件系统损坏,请先使用文件系统工具中的 "修复文件系统 (ntfsfix)" 修复。")
|
"如果是 NTFS 文件系统损坏,请先使用文件系统工具中的 '修复文件系统 (ntfsfix)' 修复。")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("错误", f"挂载设备 {device_path} 失败: {stderr}")
|
messagebox.showerror("错误", f"挂载设备 {device_path} 失败: {stderr}")
|
||||||
@@ -308,11 +308,11 @@ class DiskOperations:
|
|||||||
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
||||||
["parted", "-s", disk_path, "print"],
|
["parted", "-s", disk_path, "print"],
|
||||||
f"检查磁盘 {disk_path} 分区表失败",
|
f"检查磁盘 {disk_path} 分区表失败",
|
||||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognised disk label"),
|
||||||
root_privilege=True
|
root_privilege=True
|
||||||
)
|
)
|
||||||
if not success_check:
|
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} 没有可识别的分区表。")
|
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
|
||||||
has_partition_table = False
|
has_partition_table = False
|
||||||
else:
|
else:
|
||||||
@@ -538,7 +538,7 @@ class DiskOperations:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["losetup", "-a"],
|
["losetup", "-a"],
|
||||||
capture_output=True, text=True, check=False
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=False
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
for line in result.stdout.splitlines():
|
for line in result.stdout.splitlines():
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
block_cipher = None
|
||||||
['mainwindow_tkinter.py'],
|
|
||||||
|
|
||||||
|
a = Analysis(['main.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[('/usr/lib/python3.14/lib-dynload/_tkinter.cpython-314-x86_64-linux-gnu.so', '.')],
|
binaries=[],
|
||||||
datas=[('/usr/lib', 'tcl'), ('/usr/lib/tcl8.6', 'tk'), ('/usr/lib64/tcl8.6', 'tcltk'), ('/usr/lib/tcl8.6', 'tcltk')],
|
datas=[],
|
||||||
hiddenimports=['tkinter', 'tkinter.ttk', 'tkinter.scrolledtext', 'tkinter.messagebox', 'tkinter.filedialog', 'tkinter.simpledialog', '_tkinter'],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
excludes=[],
|
||||||
noarchive=False,
|
win_no_prefer_redirects=False,
|
||||||
optimize=0,
|
win_private_assemblies=False,
|
||||||
)
|
cipher=block_cipher,
|
||||||
pyz = PYZ(a.pure)
|
noarchive=False)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data,
|
||||||
|
cipher=block_cipher)
|
||||||
|
|
||||||
exe = EXE(
|
exe = EXE(pyz,
|
||||||
pyz,
|
|
||||||
a.scripts,
|
a.scripts,
|
||||||
a.binaries,
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
a.datas,
|
a.datas,
|
||||||
[],
|
[],
|
||||||
name='linux-storage-manager',
|
name='linux-storage-manager',
|
||||||
@@ -31,8 +35,6 @@ exe = EXE(
|
|||||||
runtime_tmpdir=None,
|
runtime_tmpdir=None,
|
||||||
console=True,
|
console=True,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=None,
|
entitlements_file=None )
|
||||||
)
|
|
||||||
|
|||||||
@@ -39,11 +39,12 @@ class LvmOperations:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
command_list,
|
command_list,
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
check=True,
|
|
||||||
encoding='utf-8',
|
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}")
|
logger.info(f"命令成功: {full_cmd_str}")
|
||||||
return True, result.stdout.strip(), result.stderr.strip()
|
return True, result.stdout.strip(), result.stderr.strip()
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ class LvmOperations:
|
|||||||
else:
|
else:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo"] + command_list if root_privilege else command_list,
|
["sudo"] + command_list if root_privilege else command_list,
|
||||||
capture_output=True, text=True, check=True,
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=True,
|
||||||
input=input_data
|
input=input_data,
|
||||||
|
stdin=subprocess.PIPE if input_data else None
|
||||||
)
|
)
|
||||||
stdout = result.stdout
|
stdout = result.stdout
|
||||||
stderr = result.stderr
|
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 dialogs_smart import SmartInfoDialog, SmartOverviewDialog
|
||||||
from smart_monitor import SmartMonitor
|
from smart_monitor import SmartMonitor
|
||||||
# 导入增强对话框
|
# 导入增强对话框
|
||||||
from dialogs_enhanced import EnhancedFormatDialog, EnhancedRaidDeleteDialog, EnhancedPartitionDialog
|
from dialogs_enhanced import (EnhancedFormatDialog, EnhancedRaidDeleteDialog,
|
||||||
|
EnhancedPartitionDialog, DependencyCheckDialog)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow:
|
class MainWindow:
|
||||||
@@ -138,6 +139,8 @@ class MainWindow:
|
|||||||
# 帮助菜单
|
# 帮助菜单
|
||||||
help_menu = tk.Menu(menubar, tearoff=0)
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
menubar.add_cascade(label="帮助", menu=help_menu)
|
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)
|
help_menu.add_command(label="关于", command=self._show_about)
|
||||||
|
|
||||||
def _show_about(self):
|
def _show_about(self):
|
||||||
@@ -155,6 +158,10 @@ class MainWindow:
|
|||||||
"使用 Python 3.6+ 和 Tkinter 构建"
|
"使用 Python 3.6+ 和 Tkinter 构建"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _show_dependency_check(self):
|
||||||
|
"""显示依赖检查对话框"""
|
||||||
|
DependencyCheckDialog(self.root)
|
||||||
|
|
||||||
def _create_block_devices_tree(self):
|
def _create_block_devices_tree(self):
|
||||||
"""创建块设备 Treeview"""
|
"""创建块设备 Treeview"""
|
||||||
# 创建框架
|
# 创建框架
|
||||||
@@ -407,6 +414,48 @@ class MainWindow:
|
|||||||
menu.add_command(label=f"擦除分区表 {device_path}...",
|
menu.add_command(label=f"擦除分区表 {device_path}...",
|
||||||
command=lambda: self._handle_wipe_partition_table(device_path))
|
command=lambda: self._handle_wipe_partition_table(device_path))
|
||||||
menu.add_separator()
|
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 信息
|
# SMART 信息
|
||||||
menu.add_command(label=f"查看 SMART 信息 {device_path}",
|
menu.add_command(label=f"查看 SMART 信息 {device_path}",
|
||||||
command=lambda: self._handle_show_smart(device_path))
|
command=lambda: self._handle_show_smart(device_path))
|
||||||
@@ -1092,8 +1141,17 @@ class MainWindow:
|
|||||||
self.refresh_all_info()
|
self.refresh_all_info()
|
||||||
|
|
||||||
def _handle_format_raid_array(self, array_path):
|
def _handle_format_raid_array(self, array_path):
|
||||||
"""处理格式化 RAID 阵列"""
|
"""处理格式化 RAID 阵列 - 使用增强型对话框"""
|
||||||
self.disk_ops.format_partition(array_path)
|
# 获取设备信息
|
||||||
|
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 处理函数 ===
|
# === LVM 处理函数 ===
|
||||||
def refresh_lvm_info(self):
|
def refresh_lvm_info(self):
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class OccupationResolver:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "lsof", device_path],
|
["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():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
logger.info(f"设备 {device_path} 被以下进程占用:\n{result.stdout}")
|
logger.info(f"设备 {device_path} 被以下进程占用:\n{result.stdout}")
|
||||||
@@ -127,7 +127,7 @@ class OccupationResolver:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "fuser", device_path],
|
["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():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
logger.info(f"设备 {device_path} 被以下进程占用: {result.stdout.strip()}")
|
logger.info(f"设备 {device_path} 被以下进程占用: {result.stdout.strip()}")
|
||||||
@@ -149,7 +149,7 @@ class OccupationResolver:
|
|||||||
# 使用 fuser -k 终止进程
|
# 使用 fuser -k 终止进程
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "fuser", "-k", device_path],
|
["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 表示没有进程被终止
|
if result.returncode == 0 or result.returncode == 1: # 1 表示没有进程被终止
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ class PartitionTableBackup:
|
|||||||
# 使用 sfdisk 备份分区表
|
# 使用 sfdisk 备份分区表
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "sfdisk", "-d", device_path],
|
["sudo", "sfdisk", "-d", device_path],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
|
encoding='utf-8',
|
||||||
check=True,
|
check=True,
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
@@ -236,8 +237,10 @@ class PartitionTableBackup:
|
|||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "sfdisk", device_path],
|
["sudo", "sfdisk", device_path],
|
||||||
input=partition_data,
|
input=partition_data,
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
encoding='utf-8',
|
||||||
check=True,
|
check=True,
|
||||||
timeout=30
|
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 logging
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,8 +105,9 @@ class SmartMonitor:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["which", "smartctl"],
|
["which", "smartctl"],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
|
encoding='utf-8',
|
||||||
check=False
|
check=False
|
||||||
)
|
)
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
@@ -99,8 +128,9 @@ class SmartMonitor:
|
|||||||
# 获取基本信息
|
# 获取基本信息
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sudo", "smartctl", "-a", device_path],
|
["sudo", "smartctl", "-a", device_path],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
|
encoding='utf-8',
|
||||||
check=False,
|
check=False,
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
@@ -232,8 +262,9 @@ class SmartMonitor:
|
|||||||
# 获取所有块设备
|
# 获取所有块设备
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["lsblk", "-d", "-n", "-o", "NAME,TYPE,ROTA"],
|
["lsblk", "-d", "-n", "-o", "NAME,TYPE,ROTA"],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
|
encoding='utf-8',
|
||||||
check=True
|
check=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -270,7 +301,7 @@ class SmartMonitor:
|
|||||||
score = 70
|
score = 70
|
||||||
|
|
||||||
# 根据错误数量调整
|
# 根据错误数量调整
|
||||||
error_count = len(smart_info.errors)
|
error_count = len(smart_info.errors) if smart_info.errors else 0
|
||||||
score -= error_count * 10
|
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 logging
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
from system_compat import get_system_compat
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -26,20 +27,26 @@ class SystemInfoManager:
|
|||||||
|
|
||||||
logger.debug(f"运行命令: {' '.join(command_list)}")
|
logger.debug(f"运行命令: {' '.join(command_list)}")
|
||||||
try:
|
try:
|
||||||
|
# Python 3.6 兼容: 使用 stdout/stderr 参数代替 capture_output
|
||||||
|
# Python 3.6 兼容: 不使用 text 参数,手动解码
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
command_list,
|
command_list,
|
||||||
capture_output=check_output,
|
stdout=subprocess.PIPE if check_output else None,
|
||||||
text=True,
|
stderr=subprocess.PIPE if check_output else None,
|
||||||
|
stdin=subprocess.PIPE if input_data else None,
|
||||||
check=True,
|
check=True,
|
||||||
encoding='utf-8',
|
encoding='utf-8' if check_output else None,
|
||||||
input=input_data
|
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:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"命令执行失败: {' '.join(command_list)}")
|
logger.error(f"命令执行失败: {' '.join(command_list)}")
|
||||||
logger.error(f"退出码: {e.returncode}")
|
logger.error(f"退出码: {e.returncode}")
|
||||||
logger.error(f"标准输出: {e.stdout.strip()}")
|
logger.error(f"标准输出: {e.stdout.strip() if e.stdout else ''}")
|
||||||
logger.error(f"标准错误: {e.stderr.strip()}")
|
logger.error(f"标准错误: {e.stderr.strip() if e.stderr else ''}")
|
||||||
raise
|
raise
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||||
@@ -52,33 +59,32 @@ class SystemInfoManager:
|
|||||||
"""
|
"""
|
||||||
使用 lsblk 获取块设备信息。
|
使用 lsblk 获取块设备信息。
|
||||||
返回一个字典列表,每个字典代表一个设备。
|
返回一个字典列表,每个字典代表一个设备。
|
||||||
|
自动适配不同系统的 lsblk 版本。
|
||||||
"""
|
"""
|
||||||
cmd = [
|
|
||||||
"lsblk", "-J", "-o",
|
|
||||||
"NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH"
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
stdout, _ = self._run_command(cmd)
|
# 使用兼容性模块获取适当的命令
|
||||||
data = json.loads(stdout)
|
compat = get_system_compat()
|
||||||
devices = data.get('blockdevices', [])
|
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:
|
for dev in dev_list:
|
||||||
if 'PATH' not in dev and 'NAME' in dev:
|
if 'path' not in dev and 'name' in dev:
|
||||||
dev['PATH'] = f"/dev/{dev['NAME']}"
|
dev['path'] = f"/dev/{dev['name']}"
|
||||||
# Rename PATH to path (lowercase) for consistency
|
|
||||||
if 'PATH' in dev:
|
if 'PATH' in dev:
|
||||||
dev['path'] = dev.pop('PATH')
|
dev['path'] = dev.pop('PATH')
|
||||||
if 'children' in dev:
|
if 'children' in dev:
|
||||||
add_path_recursive(dev['children'])
|
normalize_path_recursive(dev['children'])
|
||||||
add_path_recursive(devices)
|
|
||||||
|
normalize_path_recursive(devices)
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"获取块设备信息失败: {e.stderr.strip()}")
|
logger.error(f"获取块设备信息失败: {e.stderr.strip()}")
|
||||||
return []
|
return []
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析 lsblk JSON 输出失败: {e}")
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取块设备信息失败 (未知错误): {e}")
|
logger.error(f"获取块设备信息失败 (未知错误): {e}")
|
||||||
return []
|
return []
|
||||||
@@ -419,85 +425,50 @@ class SystemInfoManager:
|
|||||||
def get_lvm_info(self):
|
def get_lvm_info(self):
|
||||||
"""
|
"""
|
||||||
获取 LVM 物理卷 (PVs)、卷组 (VGs) 和逻辑卷 (LVs) 的信息。
|
获取 LVM 物理卷 (PVs)、卷组 (VGs) 和逻辑卷 (LVs) 的信息。
|
||||||
|
自动适配不同系统的 LVM 版本。
|
||||||
"""
|
"""
|
||||||
lvm_info = {'pvs': [], 'vgs': [], 'lvs': []}
|
lvm_info = {'pvs': [], 'vgs': [], 'lvs': []}
|
||||||
|
|
||||||
|
# 使用兼容性模块获取适当的命令
|
||||||
|
compat = get_system_compat()
|
||||||
|
commands = compat.get_lvm_commands()
|
||||||
|
|
||||||
# Get PVs
|
# Get PVs
|
||||||
try:
|
try:
|
||||||
stdout, stderr = self._run_command(["pvs", "--reportformat", "json"])
|
cmd, need_manual_parse = commands['pvs']
|
||||||
data = json.loads(stdout)
|
stdout, stderr = self._run_command(cmd)
|
||||||
if 'report' in data and data['report']:
|
lvm_info['pvs'] = compat.parse_pvs_output(stdout, need_manual_parse)
|
||||||
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')
|
|
||||||
})
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if "No physical volume found" in e.stderr or "No physical volumes found" in e.stdout:
|
if "No physical volume found" in e.stderr or "No physical volumes found" in e.stdout:
|
||||||
logger.info("未找到任何LVM物理卷。")
|
logger.info("未找到任何LVM物理卷。")
|
||||||
else:
|
else:
|
||||||
logger.error(f"获取LVM物理卷信息失败: {e.stderr.strip()}")
|
logger.error(f"获取LVM物理卷信息失败: {e.stderr.strip()}")
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析LVM物理卷JSON输出失败: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取LVM物理卷信息失败 (未知错误): {e}")
|
logger.error(f"获取LVM物理卷信息失败 (未知错误): {e}")
|
||||||
|
|
||||||
# Get VGs
|
# Get VGs
|
||||||
try:
|
try:
|
||||||
stdout, stderr = self._run_command(["vgs", "--reportformat", "json"])
|
cmd, need_manual_parse = commands['vgs']
|
||||||
data = json.loads(stdout)
|
stdout, stderr = self._run_command(cmd)
|
||||||
if 'report' in data and data['report']:
|
lvm_info['vgs'] = compat.parse_vgs_output(stdout, need_manual_parse)
|
||||||
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')
|
|
||||||
})
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if "No volume group found" in e.stderr or "No volume groups found" in e.stdout:
|
if "No volume group found" in e.stderr or "No volume groups found" in e.stdout:
|
||||||
logger.info("未找到任何LVM卷组。")
|
logger.info("未找到任何LVM卷组。")
|
||||||
else:
|
else:
|
||||||
logger.error(f"获取LVM卷组信息失败: {e.stderr.strip()}")
|
logger.error(f"获取LVM卷组信息失败: {e.stderr.strip()}")
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析LVM卷组JSON输出失败: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取LVM卷组信息失败 (未知错误): {e}")
|
logger.error(f"获取LVM卷组信息失败 (未知错误): {e}")
|
||||||
|
|
||||||
# Get LVs (MODIFIED: added -o lv_path)
|
# Get LVs
|
||||||
try:
|
try:
|
||||||
# 明确请求 lv_path,因为默认的 --reportformat json 不包含它
|
cmd, need_manual_parse = commands['lvs']
|
||||||
stdout, stderr = self._run_command(["lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", "--reportformat", "json"])
|
stdout, stderr = self._run_command(cmd)
|
||||||
data = json.loads(stdout)
|
lvm_info['lvs'] = compat.parse_lvs_output(stdout, need_manual_parse)
|
||||||
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') # 现在应该能正确获取到路径了
|
|
||||||
})
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout:
|
if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout:
|
||||||
logger.info("未找到任何LVM逻辑卷。")
|
logger.info("未找到任何LVM逻辑卷。")
|
||||||
else:
|
else:
|
||||||
logger.error(f"获取LVM逻辑卷信息失败: {e.stderr.strip()}")
|
logger.error(f"获取LVM逻辑卷信息失败: {e.stderr.strip()}")
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析LVM逻辑卷JSON输出失败: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取LVM逻辑卷信息失败 (未知错误): {e}")
|
logger.error(f"获取LVM逻辑卷信息失败 (未知错误): {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user