change tk
This commit is contained in:
326
AGENTS.md
326
AGENTS.md
@@ -1,275 +1,161 @@
|
||||
# Linux 存储管理器 (Linux Storage Manager) - AI Agent Guide
|
||||
# Linux 存储管理器 (Linux Storage Manager)
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 PySide6 开发的 Linux 存储管理图形界面工具,提供磁盘管理、逻辑卷管理(LVM)和软阵列管理(RAID)三大核心功能。
|
||||
这是一个基于 PySide6 开发的 Linux 图形化存储管理工具,提供磁盘管理、逻辑卷管理(LVM)和软 RAID 管理功能。应用程序通过调用系统命令(如 `lsblk`, `parted`, `mdadm`, `lvm` 等)与 Linux 存储子系统交互。
|
||||
|
||||
**主要功能模块:**
|
||||
1. **磁盘管理** - 分区创建/删除、格式化(ext4/xfs/ntfs/fat32)、挂载/卸载、分区表擦除
|
||||
2. **LVM 管理** - 物理卷(PV)、卷组(VG)、逻辑卷(LV)的创建和管理
|
||||
3. **RAID 管理** - 软阵列创建、管理、停止/启动阵列 (支持 RAID0/1/5/6/10)
|
||||
### 主要功能模块
|
||||
|
||||
1. **块设备管理** - 查看磁盘信息、创建分区、格式化文件系统(ext4/xfs/ntfs/fat32)、挂载/卸载设备
|
||||
2. **LVM 管理** - 物理卷(PV)、卷组(VG)、逻辑卷(LV)的创建、删除和扩容
|
||||
3. **RAID 管理** - 软 RAID 阵列的创建、删除和监控(支持 RAID0/1/5/6/10)
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 说明 |
|
||||
|------|------|
|
||||
| Python 3.14+ | 主要编程语言 |
|
||||
| PySide6 | Qt6 的 Python 绑定,用于 GUI |
|
||||
| Qt Designer (.ui 文件) | UI 设计文件 |
|
||||
| PyInstaller | 打包工具,生成独立可执行文件 |
|
||||
| 系统工具 | parted, mkfs.*, mount, umount, lvm2, mdadm, lsblk, findmnt |
|
||||
- **GUI 框架**: PySide6 (Qt for Python)
|
||||
- **UI 设计**: Qt Designer (form.ui)
|
||||
- **构建工具**: PyInstaller
|
||||
- **依赖库**:
|
||||
- PySide6 - GUI 框架
|
||||
- pexpect - 交互式命令执行(用于 RAID 创建时的密码输入)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── mainwindow.py # 主窗口,UI 与业务逻辑控制器
|
||||
├── ui_form.py # 自动生成的 UI 代码 (从 form.ui 生成)
|
||||
├── form.ui # Qt Designer UI 定义文件
|
||||
├── system_info.py # 系统信息获取模块 (块设备、RAID、挂载点)
|
||||
├── disk_operations.py # 磁盘操作 (分区、格式化、挂载)
|
||||
├── lvm_operations.py # LVM 操作 (PV/VG/LV 管理)
|
||||
├── raid_operations.py # RAID 操作 (mdadm 管理)
|
||||
├── dialogs.py # 自定义对话框 (创建分区、RAID、PV/VG/LV 等)
|
||||
├── occupation_resolver.py # 设备占用解除模块 (处理挂载、进程占用)
|
||||
├── logger_config.py # 日志配置 (输出到 QTextEdit 和控制台)
|
||||
├── pyproject.toml # PySide6 项目配置
|
||||
├── disk-manager.spec # PyInstaller 打包配置 (英文版)
|
||||
├── Linux存儲管理器.spec # PyInstaller 打包配置 (中文版)
|
||||
└── build-app.sh # 打包脚本
|
||||
├── mainwindow.py # 应用程序入口,主窗口逻辑,连接各模块
|
||||
├── ui_form.py # 由 form.ui 自动生成的 UI 类
|
||||
├── form.ui # Qt Designer UI 定义文件(主界面布局)
|
||||
├── dialogs.py # 自定义对话框(分区创建、挂载、RAID/LVM 创建等)
|
||||
├── system_info.py # 系统信息获取模块(lsblk 解析等)
|
||||
├── disk_operations.py # 磁盘操作(分区、格式化、挂载管理)
|
||||
├── lvm_operations.py # LVM 操作(PV/VG/LV 管理)
|
||||
├── raid_operations.py # RAID 操作(mdadm 命令封装)
|
||||
├── occupation_resolver.py # 设备占用检测与解除(嵌套挂载、进程占用)
|
||||
├── logger_config.py # 日志配置(输出到 QTextEdit 控件)
|
||||
├── pyproject.toml # PySide6 项目配置文件
|
||||
├── disk-manager.spec # PyInstaller 打包配置
|
||||
└── build-app.sh # 构建脚本
|
||||
```
|
||||
|
||||
## 代码组织
|
||||
|
||||
### 模块依赖关系
|
||||
## 模块依赖关系
|
||||
|
||||
```
|
||||
mainwindow.py (入口)
|
||||
├── ui_form.py (自动生成的 UI)
|
||||
mainwindow.py
|
||||
├── ui_form.py (UI 布局)
|
||||
├── system_info.py (系统信息)
|
||||
├── logger_config.py (日志)
|
||||
├── disk_operations.py (磁盘操作)
|
||||
│ └── 依赖: system_info.py, lvm_operations.py
|
||||
├── lvm_operations.py (LVM 操作,提供 _execute_shell_command)
|
||||
│ └── 依赖 system_info.py, lvm_operations.py
|
||||
├── raid_operations.py (RAID 操作)
|
||||
│ └── 依赖: system_info.py
|
||||
├── occupation_resolver.py (占用解除)
|
||||
│ └── 依赖: lvm_operations.py, system_info.py
|
||||
├── dialogs.py (对话框)
|
||||
└── logger_config.py (日志)
|
||||
│ └── 依赖 system_info.py
|
||||
├── lvm_operations.py (LVM 操作)
|
||||
├── occupation_resolver.py (占用解决)
|
||||
│ └── 依赖 lvm_operations.py, system_info.py
|
||||
└── dialogs.py (对话框)
|
||||
└── 依赖 logger_config.py
|
||||
```
|
||||
|
||||
### 核心类说明
|
||||
## 代码组织规范
|
||||
|
||||
| 类名 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| `MainWindow` | mainwindow.py | 主窗口,整合所有功能,处理用户交互 |
|
||||
| `SystemInfoManager` | system_info.py | 获取系统块设备信息、RAID 信息、挂载点 |
|
||||
| `DiskOperations` | disk_operations.py | 磁盘分区、格式化、挂载操作 |
|
||||
| `LvmOperations` | lvm_operations.py | LVM 相关操作,提供统一的 shell 命令执行接口 |
|
||||
| `RaidOperations` | raid_operations.py | RAID 阵列管理 |
|
||||
| `OccupationResolver` | occupation_resolver.py | 解除设备占用 (嵌套挂载、进程占用) |
|
||||
| `FormatWorker` | disk_operations.py | 后台格式化工作线程 |
|
||||
| `QTextEditLogger` | logger_config.py | 自定义日志处理器,输出到 UI |
|
||||
### 文件命名
|
||||
- 模块文件使用下划线命名法:`disk_operations.py`, `lvm_operations.py`
|
||||
- UI 相关文件:`form.ui`(设计源文件), `ui_form.py`(自动生成)
|
||||
|
||||
### 对话框类 (dialogs.py)
|
||||
### 类命名规范
|
||||
- 主窗口类:`MainWindow`
|
||||
- UI 类:`Ui_MainWindow`(Qt 自动生成)
|
||||
- 操作类:`DiskOperations`, `LvmOperations`, `RaidOperations`
|
||||
- 对话框类:`CreatePartitionDialog`, `MountDialog`, `CreateRaidDialog`
|
||||
- 工具类:`SystemInfoManager`, `OccupationResolver`
|
||||
|
||||
- `CreatePartitionDialog` - 创建分区对话框
|
||||
- `MountDialog` - 挂载分区对话框
|
||||
- `CreateRaidDialog` - 创建 RAID 阵列对话框
|
||||
- `CreatePvDialog` - 创建物理卷对话框
|
||||
- `CreateVgDialog` - 创建卷组对话框
|
||||
- `CreateLvDialog` - 创建逻辑卷对话框
|
||||
### 命令执行模式
|
||||
所有系统命令执行都通过 `_execute_shell_command` 或 `_run_command` 方法封装,统一处理:
|
||||
- sudo 权限提升
|
||||
- 错误处理和日志记录
|
||||
- QMessageBox 错误提示(可抑制)
|
||||
|
||||
## 构建和打包
|
||||
## 构建和运行
|
||||
|
||||
### 依赖安装
|
||||
### 运行环境要求
|
||||
- Linux 操作系统(需要 root/sudo 权限执行存储管理命令)
|
||||
- Python 3.x
|
||||
- 系统依赖:`parted`, `mdadm`, `lvm2`, `dosfstools`, `e2fsprogs`, `xfsprogs`, `ntfs-3g`
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
pip install PySide6 pyinstaller pexpect
|
||||
pip install PySide6 pexpect
|
||||
```
|
||||
|
||||
### 开发运行
|
||||
|
||||
### 运行应用
|
||||
```bash
|
||||
# 直接运行
|
||||
python mainwindow.py
|
||||
|
||||
# 注意: 部分功能需要 root 权限
|
||||
sudo python mainwindow.py
|
||||
```
|
||||
注意:大部分功能需要 root 权限,建议以 sudo 运行或确保用户有 sudo 权限。
|
||||
|
||||
### 生成可执行文件
|
||||
|
||||
### 打包可执行文件
|
||||
```bash
|
||||
# 使用 PyInstaller (需要配置虚拟环境路径)
|
||||
sudo /path/to/venv/bin/pyinstaller -F --name "disk-manager" mainwindow.py
|
||||
# 使用 PyInstaller
|
||||
sudo pyinstaller -F --name "disk-manager" mainwindow.py
|
||||
|
||||
# 或使用提供的打包脚本 (需修改路径)
|
||||
bash build-app.sh
|
||||
# 或使用提供的脚本
|
||||
sudo ./build-app.sh
|
||||
```
|
||||
|
||||
打包配置文件:
|
||||
- `disk-manager.spec` - 英文名称配置
|
||||
- `Linux存儲管理器.spec` - 中文名称配置
|
||||
打包后的可执行文件位于 `dist/disk-manager`。
|
||||
|
||||
## UI 设计规范
|
||||
|
||||
### 界面布局 (form.ui)
|
||||
|
||||
主窗口包含:
|
||||
1. **刷新按钮** - 刷新所有设备信息
|
||||
2. **标签页控件** - 三个标签页:
|
||||
- 块设备概览 (treeWidget_block_devices)
|
||||
- RAID 管理 (treeWidget_raid)
|
||||
- LVM 管理 (treeWidget_lvm)
|
||||
3. **日志输出区** (logOutputTextEdit) - 显示操作日志
|
||||
|
||||
### 修改 UI 的流程
|
||||
## 开发注意事项
|
||||
|
||||
### UI 修改流程
|
||||
1. 使用 Qt Designer 编辑 `form.ui`
|
||||
2. 使用 `pyside6-uic` 重新生成 `ui_form.py`:
|
||||
```bash
|
||||
pyside6-uic form.ui -o ui_form.py
|
||||
```
|
||||
3. 注意:`ui_form.py` 是自动生成的,不要手动修改
|
||||
2. 使用 `pyside6-uic form.ui -o ui_form.py` 重新生成 Python UI 文件
|
||||
3. 或在 Qt Creator 中配置 PySide6 项目自动处理
|
||||
|
||||
## 编码规范
|
||||
### 日志系统
|
||||
- 使用 `logger_config.py` 中配置的全局 logger
|
||||
- 日志同时输出到控制台和 GUI 的 QTextEdit 控件
|
||||
- 日志级别:`logging.DEBUG`(开发时可调整)
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用 4 空格缩进
|
||||
- 函数和变量使用小写加下划线命名 (snake_case)
|
||||
- 类名使用大驼峰命名 (PascalCase)
|
||||
- 注释和日志使用中文
|
||||
### 线程安全
|
||||
- 格式化操作使用 `FormatWorker` 在后台线程执行,避免阻塞 UI
|
||||
- 通过 Qt Signal/Slot 机制与主线程通信
|
||||
|
||||
### 错误处理
|
||||
- 所有 shell 命令执行都有统一的错误处理
|
||||
- 关键错误会弹出 QMessageBox 提示用户
|
||||
- 详细错误信息写入日志
|
||||
|
||||
所有 shell 命令执行通过 `_execute_shell_command` 方法统一处理:
|
||||
- 自动处理 sudo 权限
|
||||
- 统一的错误对话框显示
|
||||
- 详细的日志记录
|
||||
## 系统命令依赖
|
||||
|
||||
示例:
|
||||
```python
|
||||
success, stdout, stderr = self.lvm_ops._execute_shell_command(
|
||||
["parted", "-s", device_path, "mklabel", "gpt"],
|
||||
f"擦除 {device_path} 上的分区表失败"
|
||||
)
|
||||
```
|
||||
应用依赖以下 Linux 命令行工具:
|
||||
|
||||
### 日志记录
|
||||
|
||||
使用标准 logging 模块,配置在 `logger_config.py`:
|
||||
- DEBUG: 详细的调试信息
|
||||
- INFO: 常规操作信息
|
||||
- WARNING: 警告信息
|
||||
- ERROR: 错误信息
|
||||
|
||||
日志同时输出到:
|
||||
1. 控制台 (标准输出)
|
||||
2. UI 的 QTextEdit 控件 (通过 QTextEditLogger)
|
||||
|
||||
## 关键实现细节
|
||||
|
||||
### 1. 权限处理
|
||||
|
||||
所有需要 root 权限的操作都通过 `sudo` 执行。项目中没有直接处理密码输入,依赖系统的 sudo 配置。
|
||||
|
||||
### 2. 异步操作
|
||||
|
||||
格式化操作使用 `QThread` + `FormatWorker` 在后台执行,避免阻塞 UI:
|
||||
```python
|
||||
# 信号定义
|
||||
formatting_finished = Signal(bool, str, str, str) # success, device, stdout, stderr
|
||||
formatting_started = Signal(str)
|
||||
```
|
||||
|
||||
### 3. 设备识别
|
||||
|
||||
- 使用 `lsblk -J` 获取块设备信息 (JSON 格式)
|
||||
- 支持通过设备路径或 maj:min 号识别设备
|
||||
- 正确处理符号链接 (如 LVM 设备、RAID 别名)
|
||||
|
||||
### 4. 占用解除
|
||||
|
||||
`OccupationResolver` 类处理复杂的设备占用情况:
|
||||
- 嵌套挂载点卸载
|
||||
- 用户进程占用检测和终止
|
||||
- 交换分区关闭
|
||||
|
||||
## 测试策略
|
||||
|
||||
当前项目没有自动化测试套件。测试主要依赖:
|
||||
|
||||
1. **手动测试** - 在真实 Linux 环境中测试各项功能
|
||||
2. **日志验证** - 通过日志输出验证操作执行
|
||||
|
||||
**测试环境要求:**
|
||||
- Linux 系统 (建议 Ubuntu/Debian 或 Arch Linux)
|
||||
- 安装必要工具: `parted`, `lvm2`, `mdadm`, `dosfstools`, `ntfs-3g`
|
||||
- 测试磁盘建议使用虚拟机或空闲磁盘,避免数据丢失
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
⚠️ **危险操作警告:**
|
||||
|
||||
1. **格式化操作** - 会永久删除数据
|
||||
2. **分区删除** - 会丢失分区数据
|
||||
3. **分区表擦除** - 会清除整个磁盘分区信息
|
||||
4. **RAID 操作** - 错误的操作可能导致阵列损坏
|
||||
|
||||
**安全准则:**
|
||||
- 所有危险操作都有确认对话框
|
||||
- 操作前会记录警告日志
|
||||
- 建议只在测试环境或已备份的系统上使用
|
||||
- 部分操作不可逆,请谨慎操作
|
||||
|
||||
## 外部依赖工具
|
||||
|
||||
应用程序依赖以下系统工具:
|
||||
|
||||
| 工具 | 用途 |
|
||||
| 功能 | 命令 |
|
||||
|------|------|
|
||||
| lsblk | 获取块设备信息 |
|
||||
| parted | 分区管理 |
|
||||
| mkfs.ext4/mkfs.xfs/mkfs.vfat/mkfs.ntfs | 文件系统格式化 |
|
||||
| mount/umount | 挂载/卸载 |
|
||||
| lvm (pvcreate/vgcreate/lvcreate 等) | LVM 管理 |
|
||||
| mdadm | RAID 管理 |
|
||||
| findmnt | 挂载点查询 |
|
||||
| fuser | 进程占用查询 |
|
||||
| stat | 设备信息获取 |
|
||||
| pexpect | 交互式命令执行 (RAID 创建) |
|
||||
| 块设备信息 | `lsblk` |
|
||||
| 分区操作 | `parted`, `partprobe` |
|
||||
| 文件系统 | `mkfs.ext4`, `mkfs.xfs`, `mkfs.ntfs`, `mkfs.vfat` |
|
||||
| 挂载管理 | `mount`, `umount`, `findmnt` |
|
||||
| RAID 管理 | `mdadm` |
|
||||
| LVM 管理 | `pvcreate`, `vgcreate`, `lvcreate`, `pvs`, `vgs`, `lvs` 等 |
|
||||
| 进程查询 | `lsof`, `fuser` |
|
||||
|
||||
## 配置说明
|
||||
## 安全考虑
|
||||
|
||||
### pyproject.toml
|
||||
1. **权限要求**: 应用需要 root/sudo 权限执行存储管理操作
|
||||
2. **危险操作确认**: 格式化、删除分区/卷等操作都有确认对话框
|
||||
3. **设备占用检测**: `OccupationResolver` 模块检测并解除设备占用(卸载挂载点、停止进程)后才执行操作
|
||||
4. **fstab 备份**: 修改 `/etc/fstab` 前建议用户手动备份
|
||||
|
||||
PySide6 项目配置文件,定义项目文件列表:
|
||||
```toml
|
||||
[tool.pyside6-project]
|
||||
files = ["dialogs.py", "disk_operations.py", ...]
|
||||
```
|
||||
## 已知限制
|
||||
|
||||
### .gitignore
|
||||
- 仅支持 Linux 系统
|
||||
- 需要图形桌面环境
|
||||
- 部分操作(如 RAID 创建)可能需要密码输入
|
||||
- 不支持 LVM/RAID 的降级恢复等高级场景
|
||||
|
||||
排除目录:
|
||||
- /dist/ - PyInstaller 输出
|
||||
- /build/ - 构建缓存
|
||||
- /.qtcreator/ - Qt Creator 配置
|
||||
- /__pycache__/ - Python 缓存
|
||||
## 调试建议
|
||||
|
||||
## 开发工作流
|
||||
|
||||
1. **修改 UI**: 编辑 `form.ui` → 运行 `pyside6-uic` 生成 `ui_form.py`
|
||||
2. **添加功能**: 在相应模块添加逻辑,在 `MainWindow` 添加槽函数
|
||||
3. **测试**: 在 Linux 环境中运行 `python mainwindow.py`
|
||||
4. **打包**: 运行 `build-app.sh` 或直接使用 PyInstaller
|
||||
5. **提交**: 使用 `up+.sh` 脚本辅助 git 提交 (需配置)
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **UI 更新不生效** - 检查是否重新生成了 `ui_form.py`
|
||||
2. **权限错误** - 确保以 sudo 运行或配置 sudo 免密
|
||||
3. **命令未找到** - 安装对应的系统工具包
|
||||
4. **RAID 创建卡住** - pexpect 交互处理可能需要调整
|
||||
1. 查看日志输出(GUI 底部 QTextEdit 或控制台)
|
||||
2. 检查系统命令是否安装:`which parted mdadm lvm`
|
||||
3. 手动测试命令:`sudo lsblk -J`
|
||||
4. 检查权限:确保用户有 sudo 权限或已切换到 root
|
||||
|
||||
169
README_Tkinter.md
Normal file
169
README_Tkinter.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Linux 存储管理工具 - Tkinter 版本
|
||||
|
||||
这是一个使用 Tkinter 重写的 Linux 存储管理工具,适配低版本系统(如 CentOS 8)。
|
||||
|
||||
## 特性
|
||||
|
||||
- **无需 PySide6**: 使用 Python 内置的 Tkinter 模块,无需安装额外的 GUI 库
|
||||
- **兼容旧系统**: 支持 Python 3.6+,兼容 CentOS 8/RHEL 8 等系统
|
||||
- **完整功能**: 保留原版本的所有功能,包括:
|
||||
- 块设备管理(查看、创建分区、格式化、挂载/卸载)
|
||||
- RAID 管理(创建、停止、删除阵列)
|
||||
- LVM 管理(PV/VG/LV 的创建和删除)
|
||||
- 设备占用解除
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Python 3.6 或更高版本
|
||||
- tkinter 模块(通常随 Python 一起安装)
|
||||
- 以下系统工具(根据功能需求):
|
||||
- `lsblk` - 块设备信息
|
||||
- `parted` - 分区管理
|
||||
- `mdadm` - RAID 管理
|
||||
- `lvm2` - LVM 管理
|
||||
|
||||
## 安装
|
||||
|
||||
### CentOS 8 / RHEL 8
|
||||
|
||||
```bash
|
||||
# 安装 Python 3 和 tkinter
|
||||
sudo yum install -y python3 python3-tkinter
|
||||
|
||||
# 安装必要的存储管理工具
|
||||
sudo yum install -y parted mdadm lvm2 util-linux
|
||||
|
||||
# 可选:安装 pexpect(用于交互式命令,推荐安装)
|
||||
sudo pip3 install pexpect
|
||||
```
|
||||
|
||||
### Ubuntu 20.04 / Debian 10
|
||||
|
||||
```bash
|
||||
# 安装 Python 3 和 tkinter
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-tk
|
||||
|
||||
# 安装必要的存储管理工具
|
||||
sudo apt-get install -y parted mdadm lvm2 util-linux
|
||||
|
||||
# 可选:安装 pexpect
|
||||
sudo pip3 install pexpect
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式一:使用启动脚本(推荐)
|
||||
|
||||
```bash
|
||||
chmod +x run_tkinter.sh
|
||||
./run_tkinter.sh
|
||||
```
|
||||
|
||||
### 方式二:直接运行
|
||||
|
||||
```bash
|
||||
sudo python3 main_tkinter.py
|
||||
```
|
||||
|
||||
### 方式三:创建桌面快捷方式
|
||||
|
||||
```bash
|
||||
# 创建桌面文件
|
||||
cat > ~/.local/share/applications/linux-storage-manager.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Linux 存储管理器
|
||||
Comment=Linux 存储管理工具
|
||||
Exec=python3 /path/to/diskmanager/main_tkinter.py
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Icon=drive-harddisk
|
||||
Categories=System;Filesystem;
|
||||
EOF
|
||||
```
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `main_tkinter.py` | 程序入口 |
|
||||
| `mainwindow_tkinter.py` | 主窗口实现 |
|
||||
| `dialogs_tkinter.py` | 对话框组件 |
|
||||
| `logger_config_tkinter.py` | 日志配置(Tkinter 版本)|
|
||||
| `disk_operations_tkinter.py` | 磁盘操作模块(Tkinter 版本)|
|
||||
| `raid_operations_tkinter.py` | RAID 操作模块(Tkinter 版本)|
|
||||
| `lvm_operations_tkinter.py` | LVM 操作模块(Tkinter 版本)|
|
||||
| `occupation_resolver_tkinter.py` | 设备占用解除模块(Tkinter 版本)|
|
||||
| `system_info.py` | 系统信息获取模块(无需修改)|
|
||||
| `run_tkinter.sh` | 启动脚本 |
|
||||
| `verify_tkinter.py` | 环境验证脚本 |
|
||||
|
||||
## 与原版本的区别
|
||||
|
||||
1. **GUI 框架**: PySide6 → Tkinter
|
||||
2. **线程机制**: QThread → threading.Thread
|
||||
3. **信号/槽**: Qt Signal/Slot → Python 回调函数 + queue
|
||||
4. **对话框**: QDialog → tkinter.Toplevel
|
||||
5. **消息框**: QMessageBox → tkinter.messagebox
|
||||
6. **输入框**: QInputDialog → tkinter.simpledialog
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限**: 程序需要 root 权限才能执行存储管理操作
|
||||
2. **数据安全**: 删除、格式化等操作会永久删除数据,请谨慎操作
|
||||
3. **备份**: 执行重要操作前请备份重要数据
|
||||
4. **兼容性**: 虽然适配了旧系统,但建议使用前先在测试环境验证
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 问题:无法启动,提示缺少 tkinter
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# CentOS/RHEL
|
||||
sudo yum install python3-tkinter
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install python3-tk
|
||||
```
|
||||
|
||||
### 问题:部分功能无法使用
|
||||
|
||||
**解决**: 确保安装了相应的系统工具:
|
||||
```bash
|
||||
# CentOS/RHEL
|
||||
sudo yum install parted mdadm lvm2
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install parted mdadm lvm2
|
||||
```
|
||||
|
||||
### 问题:界面显示异常
|
||||
|
||||
**解决**: 尝试设置环境变量:
|
||||
```bash
|
||||
export DISPLAY=:0
|
||||
python3 main_tkinter.py
|
||||
```
|
||||
|
||||
### 问题:提示缺少 pexpect
|
||||
|
||||
**解决**: pexpect 是可选依赖,用于处理交互式命令(如创建 RAID 时的确认提示)。
|
||||
```bash
|
||||
sudo pip3 install pexpect
|
||||
```
|
||||
|
||||
如果没有安装 pexpect,RAID 创建功能仍能工作,但可能无法处理交互式提示。
|
||||
|
||||
## 开发信息
|
||||
|
||||
- **目标 Python 版本**: 3.6+
|
||||
- **GUI 库**: Tkinter (内置)
|
||||
- **线程库**: threading (内置)
|
||||
- **依赖**:
|
||||
- 必需: 无(纯 Python 标准库 + tkinter)
|
||||
- 可选: pexpect(用于交互式命令)
|
||||
|
||||
## 许可证
|
||||
|
||||
与原项目相同。
|
||||
557
dialogs_tkinter.py
Normal file
557
dialogs_tkinter.py
Normal file
@@ -0,0 +1,557 @@
|
||||
# dialogs_tkinter.py
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, simpledialog
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseDialog:
|
||||
"""对话框基类"""
|
||||
|
||||
def __init__(self, parent, title, width=400, height=300):
|
||||
self.parent = parent
|
||||
self.result = None
|
||||
|
||||
# 创建顶层窗口
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title(title)
|
||||
self.dialog.geometry(f"{width}x{height}")
|
||||
self.dialog.transient(parent) # 设置父窗口
|
||||
self.dialog.grab_set() # 模态对话框
|
||||
|
||||
# 居中显示
|
||||
self._center_window()
|
||||
|
||||
# 创建主框架
|
||||
self.main_frame = ttk.Frame(self.dialog, padding="10")
|
||||
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _center_window(self):
|
||||
"""将窗口居中显示"""
|
||||
self.dialog.update_idletasks()
|
||||
width = self.dialog.winfo_width()
|
||||
height = self.dialog.winfo_height()
|
||||
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_button_box(self):
|
||||
"""创建确定/取消按钮"""
|
||||
btn_frame = ttk.Frame(self.main_frame)
|
||||
btn_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
self.confirm_btn = ttk.Button(btn_frame, text="确定", command=self._on_confirm)
|
||||
self.confirm_btn.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
self.cancel_btn = ttk.Button(btn_frame, text="取消", command=self._on_cancel)
|
||||
self.cancel_btn.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# 绑定回车键和 ESC 键
|
||||
self.dialog.bind('<Return>', lambda e: self._on_confirm())
|
||||
self.dialog.bind('<Escape>', lambda e: self._on_cancel())
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调 - 子类重写"""
|
||||
pass
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮回调"""
|
||||
self.dialog.destroy()
|
||||
|
||||
def wait_for_result(self):
|
||||
"""等待对话框关闭并返回结果"""
|
||||
self.dialog.wait_window()
|
||||
return self.result
|
||||
|
||||
|
||||
class CreatePartitionDialog(BaseDialog):
|
||||
"""创建分区对话框"""
|
||||
|
||||
def __init__(self, parent, disk_path="", total_disk_mib=0.0, max_available_mib=0.0):
|
||||
self.disk_path = disk_path
|
||||
self.total_disk_mib = total_disk_mib
|
||||
self.max_available_mib = max_available_mib
|
||||
|
||||
super().__init__(parent, "创建分区", 450, 280)
|
||||
|
||||
logger.debug(f"CreatePartitionDialog initialized for {disk_path}. Total MiB: {total_disk_mib}, Max Available MiB: {max_available_mib}")
|
||||
|
||||
self._setup_ui()
|
||||
self._initialize_state()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 磁盘路径
|
||||
ttk.Label(content_frame, text="磁盘路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
ttk.Label(content_frame, text=self.disk_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 分区表类型
|
||||
ttk.Label(content_frame, text="分区表类型:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.part_type_var = tk.StringVar(value="gpt")
|
||||
self.part_type_combo = ttk.Combobox(content_frame, textvariable=self.part_type_var,
|
||||
values=["gpt", "msdos"], state="readonly", width=15)
|
||||
self.part_type_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 使用最大空间复选框
|
||||
self.use_max_var = tk.BooleanVar(value=True)
|
||||
self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间",
|
||||
variable=self.use_max_var,
|
||||
command=self._toggle_size_input)
|
||||
self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 分区大小
|
||||
ttk.Label(content_frame, text="分区大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
calculated_max_gb = self.max_available_mib / 1024.0
|
||||
self.max_gb = max(0.01, calculated_max_gb)
|
||||
|
||||
self.size_var = tk.DoubleVar(value=self.max_gb)
|
||||
self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=self.max_gb,
|
||||
increment=0.01, textvariable=self.size_var, width=12)
|
||||
self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
logger.debug(f"Size spinbox max set to: {self.max_gb} GB (calculated from {calculated_max_gb} GB)")
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _initialize_state(self):
|
||||
"""初始化状态"""
|
||||
self._toggle_size_input()
|
||||
|
||||
def _toggle_size_input(self):
|
||||
"""切换大小输入框状态"""
|
||||
is_checked = self.use_max_var.get()
|
||||
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
|
||||
|
||||
if is_checked:
|
||||
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
|
||||
self.size_spin.configure(state=tk.DISABLED)
|
||||
self.size_var.set(self.max_gb)
|
||||
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.")
|
||||
else:
|
||||
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
|
||||
self.size_spin.configure(state=tk.NORMAL)
|
||||
current_val = self.size_var.get()
|
||||
if current_val >= self.max_gb:
|
||||
self.size_var.set(0.01)
|
||||
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.")
|
||||
else:
|
||||
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.")
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
size_gb = self.size_var.get()
|
||||
use_max_space = self.use_max_var.get()
|
||||
|
||||
if not use_max_space and size_gb <= 0:
|
||||
messagebox.showwarning("输入错误", "分区大小必须大于0。")
|
||||
return
|
||||
|
||||
if not use_max_space and size_gb > self.max_gb:
|
||||
messagebox.showwarning("输入错误", "分区大小不能超过最大可用空间。")
|
||||
return
|
||||
|
||||
self.result = {
|
||||
'disk_path': self.disk_path,
|
||||
'partition_table_type': self.part_type_var.get(),
|
||||
'size_gb': size_gb,
|
||||
'total_disk_mib': self.total_disk_mib,
|
||||
'use_max_space': use_max_space
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_partition_info(self):
|
||||
"""获取分区信息"""
|
||||
return self.result
|
||||
|
||||
|
||||
class MountDialog(BaseDialog):
|
||||
"""挂载对话框"""
|
||||
|
||||
def __init__(self, parent, device_path=""):
|
||||
self.device_path = device_path
|
||||
super().__init__(parent, "挂载分区", 400, 200)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 设备路径
|
||||
ttk.Label(content_frame, text="设备路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
ttk.Label(content_frame, text=self.device_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 挂载点
|
||||
ttk.Label(content_frame, text="挂载点:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.mount_point_var = tk.StringVar(value=f"/mnt/{os.path.basename(self.device_path)}")
|
||||
self.mount_point_entry = ttk.Entry(content_frame, textvariable=self.mount_point_var, width=30)
|
||||
self.mount_point_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 开机自动挂载复选框
|
||||
self.add_to_fstab_var = tk.BooleanVar(value=False)
|
||||
self.fstab_check = ttk.Checkbutton(content_frame, text="开机自动挂载 (添加到 /etc/fstab)",
|
||||
variable=self.add_to_fstab_var)
|
||||
self.fstab_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
mount_point = self.mount_point_var.get().strip()
|
||||
|
||||
if not mount_point:
|
||||
messagebox.showwarning("输入错误", "挂载点不能为空。")
|
||||
return
|
||||
|
||||
if not mount_point.startswith('/'):
|
||||
messagebox.showwarning("输入错误", "挂载点必须是绝对路径 (以 '/' 开头)。")
|
||||
return
|
||||
|
||||
self.result = {
|
||||
'mount_point': mount_point,
|
||||
'add_to_fstab': self.add_to_fstab_var.get()
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_mount_info(self):
|
||||
"""获取挂载信息"""
|
||||
return self.result
|
||||
|
||||
|
||||
class CreateRaidDialog(BaseDialog):
|
||||
"""创建 RAID 阵列对话框"""
|
||||
|
||||
def __init__(self, parent, available_devices=None):
|
||||
self.available_devices = available_devices if available_devices is not None else []
|
||||
self.selected_devices = []
|
||||
|
||||
super().__init__(parent, "创建 RAID 阵列", 450, 450)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# RAID 级别
|
||||
ttk.Label(content_frame, text="RAID 级别:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.raid_level_var = tk.StringVar(value="raid0")
|
||||
self.raid_level_combo = ttk.Combobox(content_frame, textvariable=self.raid_level_var,
|
||||
values=["raid0", "raid1", "raid5", "raid6", "raid10"],
|
||||
state="readonly", width=12)
|
||||
self.raid_level_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# Chunk 大小
|
||||
ttk.Label(content_frame, text="Chunk 大小 (KB):").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.chunk_size_var = tk.IntVar(value=512)
|
||||
self.chunk_size_spin = tk.Spinbox(content_frame, from_=4, to=1024, increment=4,
|
||||
textvariable=self.chunk_size_var, width=10)
|
||||
self.chunk_size_spin.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 设备选择
|
||||
ttk.Label(content_frame, text="选择设备:").grid(row=2, column=0, sticky=tk.NW, padx=5, pady=5)
|
||||
|
||||
# 创建设备选择框架
|
||||
device_frame = ttk.Frame(content_frame)
|
||||
device_frame.grid(row=2, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 设备复选框列表
|
||||
self.device_vars = {}
|
||||
for i, dev in enumerate(self.available_devices):
|
||||
var = tk.BooleanVar(value=False)
|
||||
self.device_vars[dev] = var
|
||||
cb = ttk.Checkbutton(device_frame, text=dev, variable=var)
|
||||
cb.pack(anchor=tk.W)
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
# 收集选中的设备
|
||||
self.selected_devices = [dev for dev, var in self.device_vars.items() if var.get()]
|
||||
|
||||
if not self.selected_devices:
|
||||
messagebox.showwarning("输入错误", "请选择至少一个设备来创建 RAID 阵列。")
|
||||
return
|
||||
|
||||
raid_level = self.raid_level_var.get()
|
||||
num_devices = len(self.selected_devices)
|
||||
|
||||
# RAID 级别检查
|
||||
if raid_level == "raid0" and num_devices < 1:
|
||||
messagebox.showwarning("输入错误", "RAID0 至少需要一个设备。")
|
||||
return
|
||||
elif raid_level == "raid1" and num_devices < 2:
|
||||
messagebox.showwarning("输入错误", "RAID1 至少需要两个设备。")
|
||||
return
|
||||
elif raid_level == "raid5" and num_devices < 3:
|
||||
messagebox.showwarning("输入错误", "RAID5 至少需要三个设备。")
|
||||
return
|
||||
elif raid_level == "raid6" and num_devices < 4:
|
||||
messagebox.showwarning("输入错误", "RAID6 至少需要四个设备。")
|
||||
return
|
||||
elif raid_level == "raid10" and num_devices < 2:
|
||||
messagebox.showwarning("输入错误", "RAID10 至少需要两个设备。")
|
||||
return
|
||||
|
||||
self.result = {
|
||||
'devices': self.selected_devices,
|
||||
'level': raid_level.replace('raid', ''),
|
||||
'chunk_size': self.chunk_size_var.get()
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_raid_info(self):
|
||||
"""获取 RAID 信息"""
|
||||
return self.result
|
||||
|
||||
|
||||
class CreatePvDialog(BaseDialog):
|
||||
"""创建物理卷 (PV) 对话框"""
|
||||
|
||||
def __init__(self, parent, available_partitions=None):
|
||||
self.available_partitions = available_partitions if available_partitions is not None else []
|
||||
|
||||
super().__init__(parent, "创建物理卷 (PV)", 400, 200)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 设备选择
|
||||
ttk.Label(content_frame, text="选择设备:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
self.device_var = tk.StringVar()
|
||||
if self.available_partitions:
|
||||
self.device_var.set(self.available_partitions[0])
|
||||
|
||||
self.device_combo = ttk.Combobox(content_frame, textvariable=self.device_var,
|
||||
values=self.available_partitions, state="readonly", width=30)
|
||||
self.device_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
device_path = self.device_var.get()
|
||||
|
||||
if not device_path:
|
||||
messagebox.showwarning("输入错误", "请选择一个设备来创建物理卷。")
|
||||
return
|
||||
|
||||
self.result = {'device_path': device_path}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_pv_info(self):
|
||||
"""获取 PV 信息"""
|
||||
return self.result
|
||||
|
||||
|
||||
class CreateVgDialog(BaseDialog):
|
||||
"""创建卷组 (VG) 对话框"""
|
||||
|
||||
def __init__(self, parent, available_pvs=None):
|
||||
self.available_pvs = available_pvs if available_pvs is not None else []
|
||||
self.selected_pvs = []
|
||||
|
||||
super().__init__(parent, "创建卷组 (VG)", 450, 350)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 卷组名称
|
||||
ttk.Label(content_frame, text="卷组名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.vg_name_var = tk.StringVar()
|
||||
self.vg_name_entry = ttk.Entry(content_frame, textvariable=self.vg_name_var, width=25)
|
||||
self.vg_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 物理卷选择
|
||||
ttk.Label(content_frame, text="选择物理卷:").grid(row=1, column=0, sticky=tk.NW, padx=5, pady=5)
|
||||
|
||||
pv_frame = ttk.Frame(content_frame)
|
||||
pv_frame.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
self.pv_vars = {}
|
||||
for pv in self.available_pvs:
|
||||
var = tk.BooleanVar(value=False)
|
||||
self.pv_vars[pv] = var
|
||||
cb = ttk.Checkbutton(pv_frame, text=pv, variable=var)
|
||||
cb.pack(anchor=tk.W)
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
vg_name = self.vg_name_var.get().strip()
|
||||
self.selected_pvs = [pv for pv, var in self.pv_vars.items() if var.get()]
|
||||
|
||||
if not vg_name:
|
||||
messagebox.showwarning("输入错误", "卷组名称不能为空。")
|
||||
return
|
||||
|
||||
if not self.selected_pvs:
|
||||
messagebox.showwarning("输入错误", "请选择至少一个物理卷来创建卷组。")
|
||||
return
|
||||
|
||||
self.result = {
|
||||
'vg_name': vg_name,
|
||||
'pvs': self.selected_pvs
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_vg_info(self):
|
||||
"""获取 VG 信息"""
|
||||
return self.result
|
||||
|
||||
|
||||
class CreateLvDialog(BaseDialog):
|
||||
"""创建逻辑卷 (LV) 对话框"""
|
||||
|
||||
def __init__(self, parent, available_vgs=None, vg_sizes=None):
|
||||
self.available_vgs = available_vgs if available_vgs is not None else []
|
||||
self.vg_sizes = vg_sizes if vg_sizes is not None else {}
|
||||
|
||||
super().__init__(parent, "创建逻辑卷", 450, 300)
|
||||
|
||||
logger.debug(f"CreateLvDialog initialized. Available VGs: {self.available_vgs}, VG Sizes: {self.vg_sizes}")
|
||||
|
||||
self._setup_ui()
|
||||
self._initialize_state()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置 UI"""
|
||||
# 逻辑卷名称
|
||||
# 创建内容框架
|
||||
content_frame = ttk.Frame(self.main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(content_frame, text="逻辑卷名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.lv_name_var = tk.StringVar()
|
||||
self.lv_name_entry = ttk.Entry(content_frame, textvariable=self.lv_name_var, width=25)
|
||||
self.lv_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 卷组选择
|
||||
ttk.Label(content_frame, text="选择卷组:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.vg_var = tk.StringVar()
|
||||
if self.available_vgs:
|
||||
self.vg_var.set(self.available_vgs[0])
|
||||
self.vg_combo = ttk.Combobox(content_frame, textvariable=self.vg_var,
|
||||
values=self.available_vgs, state="readonly", width=25)
|
||||
self.vg_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
self.vg_combo.bind('<<ComboboxSelected>>', lambda e: self._update_size_options())
|
||||
|
||||
# 使用最大空间复选框
|
||||
self.use_max_var = tk.BooleanVar(value=True)
|
||||
self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间",
|
||||
variable=self.use_max_var,
|
||||
command=self._toggle_size_input)
|
||||
self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 逻辑卷大小
|
||||
ttk.Label(content_frame, text="大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.size_var = tk.DoubleVar(value=1.0)
|
||||
self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=100000.0, increment=0.01,
|
||||
textvariable=self.size_var, width=12)
|
||||
self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
# 按钮
|
||||
self._create_button_box()
|
||||
|
||||
def _initialize_state(self):
|
||||
"""初始化状态"""
|
||||
self._update_size_options()
|
||||
|
||||
def _update_size_options(self):
|
||||
"""根据选中的卷组更新大小选项"""
|
||||
selected_vg = self.vg_var.get()
|
||||
max_size_gb = self.vg_sizes.get(selected_vg, 0.0)
|
||||
|
||||
logger.debug(f"[_update_size_options] Selected VG: {selected_vg}, Max available GB: {max_size_gb}")
|
||||
|
||||
# 确保最大值至少大于等于最小值
|
||||
effective_max = max(0.01, max_size_gb)
|
||||
self.size_spin.configure(to=effective_max)
|
||||
self.max_gb = effective_max
|
||||
|
||||
logger.debug(f"[_update_size_options] Spinbox max set to: {effective_max} GB.")
|
||||
|
||||
# 应用复选框状态
|
||||
self._toggle_size_input()
|
||||
logger.debug("[_update_size_options] Completed.")
|
||||
|
||||
def _toggle_size_input(self):
|
||||
"""切换大小输入框状态"""
|
||||
is_checked = self.use_max_var.get()
|
||||
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
|
||||
|
||||
if is_checked:
|
||||
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
|
||||
self.size_spin.configure(state=tk.DISABLED)
|
||||
self.size_var.set(self.max_gb)
|
||||
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.")
|
||||
else:
|
||||
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
|
||||
self.size_spin.configure(state=tk.NORMAL)
|
||||
current_val = self.size_var.get()
|
||||
if current_val >= self.max_gb:
|
||||
self.size_var.set(0.01)
|
||||
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.")
|
||||
else:
|
||||
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.")
|
||||
logger.debug("[_toggle_size_input] Completed.")
|
||||
|
||||
def _on_confirm(self):
|
||||
"""确定按钮回调"""
|
||||
lv_name = self.lv_name_var.get().strip()
|
||||
vg_name = self.vg_var.get()
|
||||
size_gb = self.size_var.get()
|
||||
use_max_space = self.use_max_var.get()
|
||||
|
||||
if not lv_name:
|
||||
messagebox.showwarning("输入错误", "逻辑卷名称不能为空。")
|
||||
return
|
||||
|
||||
if not vg_name:
|
||||
messagebox.showwarning("输入错误", "请选择一个卷组。")
|
||||
return
|
||||
|
||||
if not use_max_space:
|
||||
if size_gb <= 0:
|
||||
messagebox.showwarning("输入错误", "逻辑卷大小必须大于0。")
|
||||
return
|
||||
if size_gb > self.max_gb:
|
||||
messagebox.showwarning("输入错误", "逻辑卷大小不能超过卷组的可用空间。")
|
||||
return
|
||||
|
||||
self.result = {
|
||||
'lv_name': lv_name,
|
||||
'vg_name': vg_name,
|
||||
'size_gb': size_gb,
|
||||
'use_max_space': use_max_space
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def get_lv_info(self):
|
||||
"""获取 LV 信息"""
|
||||
return self.result
|
||||
539
disk_operations_tkinter.py
Normal file
539
disk_operations_tkinter.py
Normal file
@@ -0,0 +1,539 @@
|
||||
# disk_operations_tkinter.py
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import threading
|
||||
import queue
|
||||
from tkinter import messagebox, simpledialog
|
||||
from system_info import SystemInfoManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- 后台格式化工作线程 ---
|
||||
class FormatWorker:
|
||||
"""后台格式化工作线程"""
|
||||
|
||||
def __init__(self, device_path, fs_type, execute_shell_command_func, result_queue):
|
||||
self.device_path = device_path
|
||||
self.fs_type = fs_type
|
||||
self._execute_shell_command_func = execute_shell_command_func
|
||||
self.result_queue = result_queue
|
||||
|
||||
def run(self):
|
||||
"""在单独线程中执行格式化命令。"""
|
||||
logger.info(f"后台格式化开始: 设备 {self.device_path}, 文件系统 {self.fs_type}")
|
||||
|
||||
# 发送开始信号
|
||||
self.result_queue.put(('started', self.device_path))
|
||||
|
||||
command_list = []
|
||||
if self.fs_type == "ext4":
|
||||
command_list = ["mkfs.ext4", "-F", self.device_path]
|
||||
elif self.fs_type == "xfs":
|
||||
command_list = ["mkfs.xfs", "-f", self.device_path]
|
||||
elif self.fs_type == "ntfs":
|
||||
command_list = ["mkfs.ntfs", "-f", self.device_path]
|
||||
elif self.fs_type == "fat32":
|
||||
command_list = ["mkfs.vfat", "-F", "32", self.device_path]
|
||||
else:
|
||||
logger.error(f"不支持的文件系统类型: {self.fs_type}")
|
||||
self.result_queue.put(('finished', False, self.device_path, "", f"不支持的文件系统类型: {self.fs_type}"))
|
||||
return
|
||||
|
||||
# 调用 shell 命令执行函数
|
||||
success, stdout, stderr = self._execute_shell_command_func(
|
||||
command_list,
|
||||
f"格式化设备 {self.device_path} 为 {self.fs_type} 失败",
|
||||
root_privilege=True,
|
||||
show_dialog=False
|
||||
)
|
||||
|
||||
self.result_queue.put(('finished', success, self.device_path, stdout, stderr))
|
||||
logger.info(f"后台格式化完成: 设备 {self.device_path}, 成功: {success}")
|
||||
|
||||
|
||||
class DiskOperations:
|
||||
"""磁盘操作类 - Tkinter 版本"""
|
||||
|
||||
def __init__(self, system_manager: SystemInfoManager, lvm_ops):
|
||||
self.system_manager = system_manager
|
||||
self._lvm_ops = lvm_ops
|
||||
self.active_format_workers = {}
|
||||
self.result_queue = queue.Queue()
|
||||
self.formatting_callback = None
|
||||
self.root = None
|
||||
|
||||
def set_formatting_callback(self, callback):
|
||||
"""设置格式化完成回调函数"""
|
||||
self.formatting_callback = callback
|
||||
|
||||
def start_queue_check(self, root):
|
||||
"""启动队列检查(应在主线程中调用)"""
|
||||
self.root = root
|
||||
self._check_queue()
|
||||
|
||||
def _check_queue(self):
|
||||
"""定期检查队列"""
|
||||
try:
|
||||
while True:
|
||||
msg = self.result_queue.get_nowait()
|
||||
msg_type = msg[0]
|
||||
|
||||
if msg_type == 'started':
|
||||
device_path = msg[1]
|
||||
logger.info(f"格式化开始: {device_path}")
|
||||
|
||||
elif msg_type == 'finished':
|
||||
_, success, device_path, stdout, stderr = msg
|
||||
self._on_formatting_finished(success, device_path, stdout, stderr)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
# 继续定时检查
|
||||
if self.root:
|
||||
self.root.after(100, self._check_queue)
|
||||
|
||||
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=None, input_data=None,
|
||||
show_dialog=True):
|
||||
"""执行 shell 命令"""
|
||||
return self._lvm_ops._execute_shell_command(
|
||||
command_list, error_message, root_privilege, suppress_critical_dialog_on_stderr_match, input_data, show_dialog
|
||||
)
|
||||
|
||||
def _add_to_fstab(self, device_path, mount_point, fstype, uuid):
|
||||
"""将设备的挂载信息添加到 /etc/fstab"""
|
||||
if not uuid or not mount_point or not fstype:
|
||||
logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。")
|
||||
messagebox.showwarning("警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。")
|
||||
return False
|
||||
|
||||
fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2"
|
||||
fstab_path = "/etc/fstab"
|
||||
|
||||
try:
|
||||
# 检查 fstab 中是否已存在相同 UUID 的条目
|
||||
with open(fstab_path, 'r') as f:
|
||||
fstab_content = f.readlines()
|
||||
|
||||
for line in fstab_content:
|
||||
if f"UUID={uuid}" in line:
|
||||
logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。")
|
||||
messagebox.showinfo("信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。")
|
||||
return True
|
||||
|
||||
# 追加到 fstab
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"],
|
||||
f"将 {device_path} 添加到 {fstab_path} 失败",
|
||||
root_privilege=True
|
||||
)
|
||||
if success:
|
||||
logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。")
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功添加到 /etc/fstab。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"处理 {fstab_path} 失败: {e}")
|
||||
messagebox.showerror("错误", f"处理 {fstab_path} 失败: {e}")
|
||||
return False
|
||||
|
||||
def _remove_fstab_entry(self, device_path):
|
||||
"""从 /etc/fstab 中移除指定设备的条目"""
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
if not device_details or not device_details.get('uuid'):
|
||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。")
|
||||
return False
|
||||
uuid = device_details.get('uuid')
|
||||
|
||||
fstab_path = "/etc/fstab"
|
||||
command = ["sed", "-i", f"/{re.escape(uuid)}/d", fstab_path]
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
command,
|
||||
f"从 {fstab_path} 中删除 UUID={uuid} 的条目失败",
|
||||
root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=(
|
||||
f"sed: {fstab_path}: No such file or directory",
|
||||
f"sed: {fstab_path}: 没有那个文件或目录",
|
||||
"no changes were made"
|
||||
)
|
||||
)
|
||||
if success:
|
||||
logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。")
|
||||
return True
|
||||
else:
|
||||
if any(s in stderr for s in (
|
||||
f"sed: {fstab_path}: No such file or directory",
|
||||
f"sed: {fstab_path}: 没有那个文件或目录",
|
||||
"no changes were made",
|
||||
"No such file or directory"
|
||||
)):
|
||||
logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
||||
"""挂载指定设备到指定挂载点"""
|
||||
if not os.path.exists(mount_point):
|
||||
try:
|
||||
os.makedirs(mount_point)
|
||||
logger.info(f"创建挂载点目录: {mount_point}")
|
||||
except OSError as e:
|
||||
messagebox.showerror("错误", f"创建挂载点目录 {mount_point} 失败: {e}")
|
||||
logger.error(f"创建挂载点目录 {mount_point} 失败: {e}")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["mount", device_path, mount_point],
|
||||
f"挂载设备 {device_path} 失败"
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
||||
if add_to_fstab:
|
||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||
if device_details:
|
||||
fstype = device_details.get('fstype')
|
||||
uuid = device_details.get('uuid')
|
||||
if fstype and uuid:
|
||||
self._add_to_fstab(device_path, mount_point, fstype, uuid)
|
||||
else:
|
||||
logger.error(f"无法获取设备 {device_path} 的文件系统类型或 UUID 以添加到 fstab。")
|
||||
messagebox.showwarning("警告", f"设备 {device_path} 已挂载,但无法获取文件系统类型或 UUID 以添加到 fstab。")
|
||||
else:
|
||||
logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab。")
|
||||
messagebox.showwarning("警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def unmount_partition(self, device_path, show_dialog_on_error=True):
|
||||
"""卸载指定设备"""
|
||||
logger.info(f"尝试卸载设备 {device_path}。")
|
||||
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
|
||||
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
|
||||
show_dialog=show_dialog_on_error
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
else:
|
||||
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors)
|
||||
if is_already_unmounted_error:
|
||||
logger.info(f"设备 {device_path} 已经处于未挂载状态。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
|
||||
"""获取磁盘上最大的空闲空间块的起始位置和大小"""
|
||||
logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
|
||||
f"获取磁盘 {disk_path} 分区信息失败",
|
||||
root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label")
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}")
|
||||
return None, None
|
||||
|
||||
free_spaces = []
|
||||
lines = stdout.splitlines()
|
||||
free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)')
|
||||
|
||||
for line in lines:
|
||||
match = free_space_line_pattern.match(line)
|
||||
if match:
|
||||
try:
|
||||
start_mib = float(match.group(1))
|
||||
size_mib = float(match.group(3))
|
||||
free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib})
|
||||
except ValueError as ve:
|
||||
logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}")
|
||||
continue
|
||||
|
||||
if not free_spaces:
|
||||
logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。")
|
||||
return None, None
|
||||
|
||||
largest_free_space = max(free_spaces, key=lambda x: x['size_mib'])
|
||||
start_mib = largest_free_space['start_mib']
|
||||
size_mib = largest_free_space['size_mib']
|
||||
|
||||
logger.debug(f"磁盘 {disk_path} 的最大空闲空间块起始于 {start_mib:.2f} MiB,大小为 {size_mib:.2f} MiB。")
|
||||
return start_mib, size_mib
|
||||
|
||||
def create_partition(self, disk_path, partition_table_type, size_gb, total_disk_mib, use_max_space):
|
||||
"""在指定磁盘上创建分区"""
|
||||
if not isinstance(disk_path, str) or not isinstance(partition_table_type, str):
|
||||
logger.error(f"尝试创建分区时传入无效参数。磁盘路径: {disk_path}, 分区表类型: {partition_table_type}")
|
||||
messagebox.showerror("错误", "无效的磁盘路径或分区表类型。")
|
||||
return False
|
||||
|
||||
# 1. 检查磁盘是否有分区表
|
||||
has_partition_table = False
|
||||
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "print"],
|
||||
f"检查磁盘 {disk_path} 分区表失败",
|
||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
||||
root_privilege=True
|
||||
)
|
||||
if not success_check:
|
||||
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")):
|
||||
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
|
||||
has_partition_table = False
|
||||
else:
|
||||
logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}")
|
||||
return False
|
||||
else:
|
||||
if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check:
|
||||
has_partition_table = True
|
||||
else:
|
||||
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。")
|
||||
has_partition_table = False
|
||||
|
||||
actual_start_mib_for_parted = 0.0
|
||||
|
||||
# 2. 如果没有分区表,则创建分区表
|
||||
if not has_partition_table:
|
||||
if not messagebox.askyesno("确认创建分区表",
|
||||
f"磁盘 {disk_path} 没有分区表。您确定要创建 {partition_table_type} 分区表吗?\n"
|
||||
f"此操作将擦除磁盘上的所有数据。",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了在 {disk_path} 上创建分区表的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试在 {disk_path} 上创建 {partition_table_type} 分区表。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "mklabel", partition_table_type],
|
||||
f"创建 {partition_table_type} 分区表失败"
|
||||
)
|
||||
if not success:
|
||||
return False
|
||||
actual_start_mib_for_parted = 1.0
|
||||
else:
|
||||
start_mib_from_parted, _ = self.get_disk_free_space_info_mib(disk_path, total_disk_mib)
|
||||
if start_mib_from_parted is None:
|
||||
messagebox.showerror("错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。")
|
||||
return False
|
||||
if start_mib_from_parted < 1.0:
|
||||
actual_start_mib_for_parted = 1.0
|
||||
else:
|
||||
actual_start_mib_for_parted = start_mib_from_parted
|
||||
|
||||
# 3. 确定分区结束位置
|
||||
if use_max_space:
|
||||
end_pos = "100%"
|
||||
size_for_log = "最大可用空间"
|
||||
else:
|
||||
end_mib = actual_start_mib_for_parted + size_gb * 1024
|
||||
if end_mib > total_disk_mib:
|
||||
messagebox.showwarning("警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。")
|
||||
end_pos = "100%"
|
||||
size_for_log = "最大可用空间"
|
||||
else:
|
||||
end_pos = f"{end_mib}MiB"
|
||||
size_for_log = f"{size_gb}GB"
|
||||
|
||||
# 4. 创建分区
|
||||
create_cmd = ["parted", "-s", disk_path, "mkpart", "primary", f"{actual_start_mib_for_parted}MiB", end_pos]
|
||||
logger.info(f"尝试在 {disk_path} 上创建 {size_for_log} 的主分区。命令: {' '.join(create_cmd)}")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
create_cmd,
|
||||
f"在 {disk_path} 上创建分区失败"
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def delete_partition(self, device_path):
|
||||
"""删除指定分区"""
|
||||
if not messagebox.askyesno("确认删除分区",
|
||||
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
|
||||
return False
|
||||
|
||||
# 尝试卸载分区
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False)
|
||||
|
||||
# 从 fstab 中移除条目
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
# 获取父磁盘和分区号
|
||||
match = re.match(r'(/dev/[a-z]+)(\d+)', device_path)
|
||||
if not match:
|
||||
messagebox.showerror("错误", f"无法解析设备路径 {device_path}。")
|
||||
logger.error(f"无法解析设备路径 {device_path}。")
|
||||
return False
|
||||
|
||||
disk_path = match.group(1)
|
||||
partition_number = match.group(2)
|
||||
|
||||
logger.info(f"尝试删除分区 {device_path} (磁盘: {disk_path}, 分区号: {partition_number})。")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["parted", "-s", disk_path, "rm", partition_number],
|
||||
f"删除分区 {device_path} 失败"
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"分区 {device_path} 已成功删除。")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def format_partition(self, device_path, fstype=None):
|
||||
"""启动后台格式化线程"""
|
||||
if fstype is None:
|
||||
# 使用简单的输入对话框
|
||||
fstype = simpledialog.askstring("选择文件系统",
|
||||
f"请选择要使用的文件系统类型 for {device_path}:\n\next4, xfs, fat32, ntfs",
|
||||
initialvalue="ext4")
|
||||
if not fstype:
|
||||
logger.info("用户取消了文件系统选择。")
|
||||
return False
|
||||
|
||||
if fstype not in ["ext4", "xfs", "fat32", "ntfs"]:
|
||||
messagebox.showerror("错误", f"不支持的文件系统类型: {fstype}")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认格式化",
|
||||
f"您确定要格式化设备 {device_path} 为 {fstype} 吗?此操作将擦除所有数据!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了格式化 {device_path} 的操作。")
|
||||
return False
|
||||
|
||||
# 检查是否已有格式化任务正在进行
|
||||
if device_path in self.active_format_workers:
|
||||
messagebox.showwarning("警告", f"设备 {device_path} 正在格式化中,请勿重复操作。")
|
||||
return False
|
||||
|
||||
# 尝试卸载分区
|
||||
self.unmount_partition(device_path, show_dialog_on_error=False)
|
||||
# 从 fstab 中移除条目
|
||||
self._remove_fstab_entry(device_path)
|
||||
|
||||
# 创建并启动后台线程
|
||||
worker = FormatWorker(device_path, fstype, self._execute_shell_command, self.result_queue)
|
||||
thread = threading.Thread(target=worker.run, daemon=True)
|
||||
self.active_format_workers[device_path] = thread
|
||||
thread.start()
|
||||
|
||||
messagebox.showinfo("开始格式化", f"设备 {device_path} 正在后台格式化为 {fstype}。完成后将通知您。")
|
||||
return True
|
||||
|
||||
def _on_formatting_finished(self, success, device_path, stdout, stderr):
|
||||
"""处理格式化工作线程完成后的结果"""
|
||||
if device_path in self.active_format_workers:
|
||||
del self.active_format_workers[device_path]
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("格式化成功", f"设备 {device_path} 已成功格式化。")
|
||||
else:
|
||||
messagebox.showerror("格式化失败", f"格式化设备 {device_path} 失败。\n错误详情: {stderr}")
|
||||
|
||||
# 调用回调函数通知主窗口
|
||||
if self.formatting_callback:
|
||||
self.formatting_callback(success, device_path, stdout, stderr)
|
||||
|
||||
|
||||
# === Loop 设备挂载/卸载功能 ===
|
||||
def mount_loop_device(self, device_path):
|
||||
"""挂载 ROM 设备为 loop 设备"""
|
||||
# 获取默认挂载点
|
||||
default_mount = f"/mnt/{os.path.basename(device_path)}"
|
||||
|
||||
mount_point = simpledialog.askstring(
|
||||
"挂载 ROM 设备",
|
||||
f"请输入挂载点路径 for {device_path}:",
|
||||
initialvalue=default_mount
|
||||
)
|
||||
|
||||
if not mount_point:
|
||||
logger.info("用户取消了挂载操作。")
|
||||
return False
|
||||
|
||||
# 创建挂载点目录
|
||||
if not os.path.exists(mount_point):
|
||||
try:
|
||||
os.makedirs(mount_point)
|
||||
logger.info(f"创建挂载点目录: {mount_point}")
|
||||
except OSError as e:
|
||||
messagebox.showerror("错误", f"创建挂载点目录 {mount_point} 失败: {e}")
|
||||
return False
|
||||
|
||||
# 使用 mount -o loop 挂载
|
||||
logger.info(f"尝试以 loop 方式挂载 ROM 设备 {device_path} 到 {mount_point}")
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["mount", "-o", "loop,ro", device_path, mount_point],
|
||||
f"挂载 ROM 设备 {device_path} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"ROM 设备 {device_path} 已成功挂载到 {mount_point}")
|
||||
return True
|
||||
else:
|
||||
# 检查是否是因为设备不是有效的文件系统
|
||||
if "wrong fs type" in stderr.lower() or "bad option" in stderr.lower():
|
||||
messagebox.showerror("错误",
|
||||
f"无法挂载 {device_path}\n"
|
||||
f"可能的原因:\n"
|
||||
f"1. 设备中没有可读取的介质(如光盘未插入)\n"
|
||||
f"2. 设备不支持 loop 挂载\n\n"
|
||||
f"错误详情: {stderr}")
|
||||
return False
|
||||
|
||||
def unmount_loop_device(self, device_path):
|
||||
"""卸载 loop 设备"""
|
||||
logger.info(f"尝试卸载 loop 设备 {device_path}")
|
||||
|
||||
# 首先尝试普通卸载
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
show_dialog=False
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
|
||||
# 检查是否是因为设备未挂载
|
||||
if "not mounted" in stderr.lower() or "未挂载" in stderr:
|
||||
# 尝试查找关联的 loop 设备并卸载
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["losetup", "-a"],
|
||||
capture_output=True, text=True, check=False
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
if device_path in line:
|
||||
loop_dev = line.split(':')[0]
|
||||
logger.info(f"找到关联的 loop 设备: {loop_dev}")
|
||||
# 卸载 loop 设备
|
||||
success, _, _ = self._execute_shell_command(
|
||||
["losetup", "-d", loop_dev],
|
||||
f"删除 loop 设备 {loop_dev} 失败",
|
||||
show_dialog=False
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"Loop 设备 {loop_dev} 已卸载。")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"查找 loop 设备时出错: {e}")
|
||||
|
||||
messagebox.showerror("错误", f"卸载设备 {device_path} 失败:\n{stderr}")
|
||||
return False
|
||||
84
logger_config_tkinter.py
Normal file
84
logger_config_tkinter.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# logger_config_tkinter.py
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
import queue
|
||||
|
||||
|
||||
class TkinterLogHandler(logging.Handler):
|
||||
"""
|
||||
自定义的 logging 处理器,将日志消息发送到 Tkinter 文本控件。
|
||||
使用队列确保线程安全。
|
||||
"""
|
||||
|
||||
def __init__(self, text_widget: ScrolledText):
|
||||
super().__init__()
|
||||
self.text_widget = text_widget
|
||||
self.queue = queue.Queue()
|
||||
self._schedule_check()
|
||||
|
||||
def _schedule_check(self):
|
||||
"""定期检查队列并更新 UI"""
|
||||
self._check_queue()
|
||||
# 每 100ms 检查一次队列
|
||||
self.text_widget.after(100, self._schedule_check)
|
||||
|
||||
def _check_queue(self):
|
||||
"""处理队列中的日志消息"""
|
||||
try:
|
||||
while True:
|
||||
msg = self.queue.get_nowait()
|
||||
self._append_to_widget(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def _append_to_widget(self, msg):
|
||||
"""将消息追加到文本控件"""
|
||||
try:
|
||||
self.text_widget.configure(state=tk.NORMAL)
|
||||
self.text_widget.insert(tk.END, msg + '\n')
|
||||
self.text_widget.see(tk.END) # 滚动到底部
|
||||
self.text_widget.configure(state=tk.DISABLED)
|
||||
except Exception as e:
|
||||
# 如果控件已销毁,忽略错误
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
"""
|
||||
处理日志记录,将其格式化并放入队列。
|
||||
"""
|
||||
try:
|
||||
msg = self.format(record)
|
||||
self.queue.put(msg)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def setup_logging(text_widget: ScrolledText):
|
||||
"""
|
||||
配置全局 logging,使其输出到控制台和指定的 Tkinter 文本控件。
|
||||
"""
|
||||
root_logger = logging.getLogger()
|
||||
# 设置日志级别
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 清除现有的处理器
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# 1. 控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 2. Tkinter 文本控件处理器
|
||||
text_handler = TkinterLogHandler(text_widget)
|
||||
text_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(text_handler)
|
||||
|
||||
return root_logger
|
||||
|
||||
|
||||
# 获取一个全局的 logger 实例
|
||||
logger = logging.getLogger(__name__)
|
||||
291
lvm_operations_tkinter.py
Normal file
291
lvm_operations_tkinter.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# lvm_operations_tkinter.py
|
||||
import logging
|
||||
import subprocess
|
||||
from tkinter import messagebox
|
||||
from system_info import SystemInfoManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LvmOperations:
|
||||
"""LVM 操作类 - Tkinter 版本"""
|
||||
|
||||
def __init__(self, system_manager: SystemInfoManager = None):
|
||||
self.system_manager = system_manager
|
||||
|
||||
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
||||
suppress_critical_dialog_on_stderr_match=None, input_data=None,
|
||||
show_dialog=True):
|
||||
"""执行 shell 命令"""
|
||||
full_cmd_str = ' '.join(command_list)
|
||||
logger.info(f"执行命令: {full_cmd_str}")
|
||||
|
||||
try:
|
||||
if self.system_manager:
|
||||
stdout, stderr = self.system_manager._run_command(
|
||||
command_list,
|
||||
root_privilege=root_privilege,
|
||||
check_output=True,
|
||||
input_data=input_data
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(
|
||||
["sudo"] + command_list if root_privilege else command_list,
|
||||
capture_output=True, text=True, check=True,
|
||||
input=input_data
|
||||
)
|
||||
stdout = result.stdout
|
||||
stderr = result.stderr
|
||||
|
||||
if stdout:
|
||||
logger.debug(f"命令 '{full_cmd_str}' 标准输出:\n{stdout.strip()}")
|
||||
if stderr:
|
||||
logger.debug(f"命令 '{full_cmd_str}' 标准错误:\n{stderr.strip()}")
|
||||
|
||||
return True, stdout, stderr
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
stderr_output = e.stderr.strip() if e.stderr else ""
|
||||
logger.error(f"{error_message} 命令: {full_cmd_str}")
|
||||
logger.error(f"退出码: {e.returncode}")
|
||||
logger.error(f"标准输出: {e.stdout.strip() if e.stdout else ''}")
|
||||
logger.error(f"标准错误: {stderr_output}")
|
||||
|
||||
should_suppress_dialog = False
|
||||
if suppress_critical_dialog_on_stderr_match:
|
||||
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
||||
if suppress_critical_dialog_on_stderr_match in stderr_output:
|
||||
should_suppress_dialog = True
|
||||
elif isinstance(suppress_critical_dialog_on_stderr_match, (list, tuple)):
|
||||
for pattern in suppress_critical_dialog_on_stderr_match:
|
||||
if pattern in stderr_output:
|
||||
should_suppress_dialog = True
|
||||
break
|
||||
|
||||
if should_suppress_dialog:
|
||||
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
|
||||
elif show_dialog:
|
||||
messagebox.showerror("错误", f"{error_message}\n错误详情: {stderr_output}")
|
||||
|
||||
return False, e.stdout if e.stdout else "", stderr_output
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
if show_dialog:
|
||||
messagebox.showerror("错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
return False, "", "Command not found."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行命令时发生未知错误: {e}")
|
||||
if show_dialog:
|
||||
messagebox.showerror("错误", f"执行命令时发生未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def create_pv(self, device_path):
|
||||
"""创建物理卷"""
|
||||
if not device_path or not device_path.startswith('/dev/'):
|
||||
messagebox.showerror("错误", "无效的设备路径。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认创建物理卷",
|
||||
f"您确定要在 {device_path} 上创建物理卷吗?\n"
|
||||
"此操作将擦除设备上的数据!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了在 {device_path} 上创建物理卷的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试在 {device_path} 上创建物理卷。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["pvcreate", "-y", device_path],
|
||||
f"在 {device_path} 上创建物理卷失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"物理卷已在 {device_path} 上成功创建。")
|
||||
return True
|
||||
else:
|
||||
if "partition" in stderr.lower() or "分区" in stderr:
|
||||
if messagebox.askyesno("警告",
|
||||
f"设备 {device_path} 似乎已有分区。\n"
|
||||
"是否继续擦除并创建物理卷?",
|
||||
default=messagebox.NO):
|
||||
success_wipe, _, _ = self._execute_shell_command(
|
||||
["wipefs", "-a", device_path],
|
||||
f"擦除设备 {device_path} 失败"
|
||||
)
|
||||
if success_wipe:
|
||||
success_pv, _, stderr_pv = self._execute_shell_command(
|
||||
["pvcreate", "-y", device_path],
|
||||
f"在 {device_path} 上创建物理卷失败"
|
||||
)
|
||||
if success_pv:
|
||||
messagebox.showinfo("成功", f"物理卷已在 {device_path} 上成功创建。")
|
||||
return True
|
||||
else:
|
||||
messagebox.showerror("错误", f"在 {device_path} 上创建物理卷失败: {stderr_pv}")
|
||||
else:
|
||||
messagebox.showinfo("信息", f"未在已分区设备 {device_path} 上创建物理卷。")
|
||||
return False
|
||||
|
||||
def delete_pv(self, device_path):
|
||||
"""删除物理卷"""
|
||||
if not device_path or not device_path.startswith('/dev/'):
|
||||
messagebox.showerror("错误", "无效的设备路径。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认删除物理卷",
|
||||
f"您确定要删除物理卷 {device_path} 吗?",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除物理卷 {device_path} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除物理卷 {device_path}。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["pvremove", "-y", device_path],
|
||||
f"删除物理卷 {device_path} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"物理卷 {device_path} 已成功删除。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_vg(self, vg_name, pvs):
|
||||
"""创建卷组"""
|
||||
if not vg_name or not pvs:
|
||||
messagebox.showerror("错误", "无效的卷组名称或物理卷路径。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认创建卷组",
|
||||
f"您确定要创建卷组 {vg_name} 吗?\n"
|
||||
f"使用物理卷: {', '.join(pvs)}",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了创建卷组 {vg_name} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试创建卷组 {vg_name},使用物理卷: {pvs}")
|
||||
cmd = ["vgcreate", vg_name] + pvs
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
cmd,
|
||||
f"创建卷组 {vg_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"卷组 {vg_name} 已成功创建。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete_vg(self, vg_name):
|
||||
"""删除卷组"""
|
||||
if not vg_name:
|
||||
messagebox.showerror("错误", "无效的卷组名称。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认删除卷组",
|
||||
f"您确定要删除卷组 {vg_name} 吗?\n"
|
||||
"此操作将删除该卷组及其所有逻辑卷!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除卷组 {vg_name} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除卷组 {vg_name}。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["vgremove", "-y", vg_name],
|
||||
f"删除卷组 {vg_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"卷组 {vg_name} 已成功删除。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_lv(self, lv_name, vg_name, size_gb, use_max_space=False):
|
||||
"""创建逻辑卷"""
|
||||
if not lv_name or not vg_name:
|
||||
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
|
||||
return False
|
||||
|
||||
if not use_max_space and size_gb <= 0:
|
||||
messagebox.showerror("错误", "逻辑卷大小必须大于0。")
|
||||
return False
|
||||
|
||||
size_str = "100%FREE" if use_max_space else f"{size_gb}G"
|
||||
|
||||
if not messagebox.askyesno("确认创建逻辑卷",
|
||||
f"您确定要在卷组 {vg_name} 中创建逻辑卷 {lv_name} 吗?\n"
|
||||
f"大小: {size_str}",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了创建逻辑卷 {lv_name} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试在卷组 {vg_name} 中创建逻辑卷 {lv_name},大小: {size_str}")
|
||||
cmd = ["lvcreate", "-y", "-n", lv_name, "-L", size_str, vg_name] if not use_max_space else \
|
||||
["lvcreate", "-y", "-n", lv_name, "-l", "100%FREE", vg_name]
|
||||
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
cmd,
|
||||
f"创建逻辑卷 {lv_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"逻辑卷 {lv_name} 已在卷组 {vg_name} 中成功创建。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete_lv(self, lv_name, vg_name):
|
||||
"""删除逻辑卷"""
|
||||
if not lv_name or not vg_name:
|
||||
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认删除逻辑卷",
|
||||
f"您确定要删除逻辑卷 {vg_name}/{lv_name} 吗?\n"
|
||||
"此操作将删除该逻辑卷及其所有数据!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除逻辑卷 {lv_name} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试删除逻辑卷 {vg_name}/{lv_name}。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["lvremove", "-y", f"{vg_name}/{lv_name}"],
|
||||
f"删除逻辑卷 {vg_name}/{lv_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功删除。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def activate_lv(self, lv_name, vg_name):
|
||||
"""激活逻辑卷"""
|
||||
if not lv_name or not vg_name:
|
||||
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试激活逻辑卷 {vg_name}/{lv_name}。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["lvchange", "-ay", f"{vg_name}/{lv_name}"],
|
||||
f"激活逻辑卷 {vg_name}/{lv_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功激活。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def deactivate_lv(self, lv_name, vg_name):
|
||||
"""停用逻辑卷"""
|
||||
if not lv_name or not vg_name:
|
||||
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试停用逻辑卷 {vg_name}/{lv_name}。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["lvchange", "-an", f"{vg_name}/{lv_name}"],
|
||||
f"停用逻辑卷 {vg_name}/{lv_name} 失败"
|
||||
)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功停用。")
|
||||
return True
|
||||
return False
|
||||
43
main_tkinter.py
Normal file
43
main_tkinter.py
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Linux 存储管理工具 - Tkinter 版本
|
||||
适配低版本系统(如 CentOS 8)
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将当前目录添加到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from mainwindow_tkinter import MainWindow
|
||||
|
||||
|
||||
def main():
|
||||
"""主程序入口"""
|
||||
root = tk.Tk()
|
||||
root.title("Linux 存储管理工具")
|
||||
root.geometry("1200x800")
|
||||
|
||||
# 设置最小窗口大小
|
||||
root.minsize(800, 600)
|
||||
|
||||
# 尝试设置 DPI 感知(仅在 Windows 上有效)
|
||||
try:
|
||||
from ctypes import windll
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建应用
|
||||
app = MainWindow(root)
|
||||
|
||||
# 运行主循环
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1053
mainwindow_tkinter.py
Normal file
1053
mainwindow_tkinter.py
Normal file
File diff suppressed because it is too large
Load Diff
179
occupation_resolver_tkinter.py
Normal file
179
occupation_resolver_tkinter.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# occupation_resolver_tkinter.py
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
from tkinter import messagebox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OccupationResolver:
|
||||
"""设备占用解除类 - Tkinter 版本"""
|
||||
|
||||
def __init__(self, shell_executor_func, mount_info_getter_func):
|
||||
self.shell_executor_func = shell_executor_func
|
||||
self.mount_info_getter_func = mount_info_getter_func
|
||||
|
||||
def resolve_occupation(self, device_path: str) -> bool:
|
||||
"""处理设备占用问题"""
|
||||
logger.info(f"开始处理设备 {device_path} 的占用问题。")
|
||||
|
||||
# 1. 获取挂载点
|
||||
mount_point = self.mount_info_getter_func(device_path)
|
||||
|
||||
# 2. 如果设备已挂载,尝试卸载
|
||||
if mount_point:
|
||||
logger.info(f"设备 {device_path} 已挂载在 {mount_point}。检查是否有嵌套挂载...")
|
||||
|
||||
# 卸载嵌套挂载点
|
||||
if not self._unmount_nested_mounts(mount_point):
|
||||
messagebox.showerror("错误", f"未能卸载设备 {device_path} 下的所有嵌套挂载点。")
|
||||
# 嵌套挂载卸载失败,可能是被进程占用,继续检查进程
|
||||
else:
|
||||
# 嵌套挂载卸载成功,尝试卸载设备本身
|
||||
if self._unmount_device(device_path):
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 及其所有挂载点已成功卸载。")
|
||||
return True
|
||||
# 设备卸载失败,可能是被进程占用,继续检查进程
|
||||
logger.warning(f"设备 {device_path} 卸载失败,可能存在进程占用。")
|
||||
else:
|
||||
logger.info(f"设备 {device_path} 当前未挂载。")
|
||||
|
||||
# 3. 检查是否有进程占用设备(无论是否已挂载,都检查进程占用)
|
||||
if self._is_device_busy(device_path):
|
||||
return self._kill_processes_using_device(device_path)
|
||||
|
||||
# 4. 如果之前有挂载点但卸载失败,且没有进程占用,说明是其他原因
|
||||
if mount_point:
|
||||
messagebox.showwarning("警告",
|
||||
f"设备 {device_path} 卸载失败,但未检测到进程占用。\n"
|
||||
f"可能是其他原因导致,请检查日志。")
|
||||
return False
|
||||
|
||||
# 5. 无任何占用
|
||||
messagebox.showinfo("信息", f"设备 {device_path} 未被任何进程占用。")
|
||||
return True
|
||||
|
||||
def _unmount_nested_mounts(self, mount_point: str) -> bool:
|
||||
"""卸载指定挂载点下的所有嵌套挂载"""
|
||||
logger.info(f"尝试卸载 {mount_point} 下的所有嵌套挂载点。")
|
||||
|
||||
try:
|
||||
with open('/proc/mounts', 'r') as f:
|
||||
mounts = f.readlines()
|
||||
|
||||
nested_mounts = []
|
||||
for line in mounts:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
mp = parts[1]
|
||||
if mp.startswith(mount_point + '/') or mp == mount_point:
|
||||
nested_mounts.append(mp)
|
||||
|
||||
# 按深度排序,先卸载深层挂载
|
||||
nested_mounts.sort(key=lambda x: x.count('/'), reverse=True)
|
||||
|
||||
for nm in nested_mounts:
|
||||
logger.info(f"尝试卸载嵌套挂载点: {nm}")
|
||||
success, _, _ = self.shell_executor_func(
|
||||
["umount", nm],
|
||||
f"卸载嵌套挂载点 {nm} 失败",
|
||||
show_dialog=False
|
||||
)
|
||||
if not success:
|
||||
logger.warning(f"卸载嵌套挂载点 {nm} 失败,但将继续尝试其他挂载点。")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"卸载嵌套挂载点时发生错误: {e}")
|
||||
return False
|
||||
|
||||
def _unmount_device(self, device_path: str) -> bool:
|
||||
"""卸载设备"""
|
||||
logger.info(f"尝试卸载设备 {device_path}。")
|
||||
success, _, stderr = self.shell_executor_func(
|
||||
["umount", device_path],
|
||||
f"卸载设备 {device_path} 失败",
|
||||
show_dialog=False
|
||||
)
|
||||
if success:
|
||||
logger.info(f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
else:
|
||||
# 检查是否是因为设备已经未挂载
|
||||
if "未挂载" in stderr or "not mounted" in stderr.lower():
|
||||
logger.info(f"设备 {device_path} 已经处于未挂载状态。")
|
||||
return True
|
||||
logger.error(f"卸载设备 {device_path} 失败: {stderr}")
|
||||
return False
|
||||
|
||||
def _is_device_busy(self, device_path: str) -> bool:
|
||||
"""检查设备是否被进程占用"""
|
||||
logger.info(f"检查设备 {device_path} 是否被进程占用。")
|
||||
|
||||
# 尝试使用 lsof
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", "lsof", device_path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
logger.info(f"设备 {device_path} 被以下进程占用:\n{result.stdout}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"lsof 检查失败: {e}")
|
||||
|
||||
# 尝试使用 fuser
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", "fuser", device_path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
logger.info(f"设备 {device_path} 被以下进程占用: {result.stdout.strip()}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"fuser 检查失败: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def _kill_processes_using_device(self, device_path: str) -> bool:
|
||||
"""终止占用设备的进程"""
|
||||
logger.info(f"尝试终止占用设备 {device_path} 的进程。")
|
||||
|
||||
if messagebox.askyesno("确认终止进程",
|
||||
f"设备 {device_path} 当前被进程占用。\n"
|
||||
"是否终止这些进程?",
|
||||
default=messagebox.NO):
|
||||
try:
|
||||
# 使用 fuser -k 终止进程
|
||||
result = subprocess.run(
|
||||
["sudo", "fuser", "-k", device_path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0 or result.returncode == 1: # 1 表示没有进程被终止
|
||||
messagebox.showinfo("成功", f"已尝试终止占用设备 {device_path} 的所有进程。")
|
||||
|
||||
# 再次尝试卸载
|
||||
if self._unmount_device(device_path):
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
else:
|
||||
# 设备可能已经没有挂载
|
||||
mount_point = self.mount_info_getter_func(device_path)
|
||||
if not mount_point:
|
||||
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
|
||||
return True
|
||||
messagebox.showwarning("警告", f"进程已终止,但未能成功卸载设备 {device_path}。可能仍有其他问题。")
|
||||
return False
|
||||
else:
|
||||
messagebox.showerror("错误", f"未能终止占用设备 {device_path} 的进程。")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"终止进程时发生错误: {e}")
|
||||
messagebox.showerror("错误", f"终止进程时发生错误: {e}")
|
||||
return False
|
||||
else:
|
||||
messagebox.showinfo("信息", f"已取消终止占用设备 {device_path} 的进程。")
|
||||
return False
|
||||
410
raid_operations_tkinter.py
Normal file
410
raid_operations_tkinter.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# raid_operations_tkinter.py
|
||||
import logging
|
||||
import subprocess
|
||||
from tkinter import messagebox, simpledialog
|
||||
from system_info import SystemInfoManager
|
||||
import re
|
||||
try:
|
||||
import pexpect
|
||||
PEXPECT_AVAILABLE = True
|
||||
except ImportError:
|
||||
PEXPECT_AVAILABLE = False
|
||||
pexpect = None
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RaidOperations:
|
||||
def __init__(self, system_manager: SystemInfoManager):
|
||||
self.system_manager = system_manager
|
||||
|
||||
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
|
||||
"""执行一个shell命令并返回stdout和stderr"""
|
||||
full_cmd_str = ' '.join(command_list)
|
||||
logger.info(f"执行命令: {full_cmd_str}")
|
||||
|
||||
try:
|
||||
stdout, stderr = self.system_manager._run_command(
|
||||
command_list,
|
||||
root_privilege=True,
|
||||
check_output=True,
|
||||
input_data=input_to_command
|
||||
)
|
||||
|
||||
if stdout: logger.debug(f"命令 '{full_cmd_str}' 标准输出:\n{stdout.strip()}")
|
||||
if stderr: logger.debug(f"命令 '{full_cmd_str}' 标准错误:\n{stderr.strip()}")
|
||||
|
||||
return True, stdout, stderr
|
||||
except subprocess.CalledProcessError as e:
|
||||
stderr_output = e.stderr.strip()
|
||||
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}")
|
||||
logger.error(f"退出码: {e.returncode}")
|
||||
logger.error(f"标准输出: {e.stdout.strip()}")
|
||||
logger.error(f"标准错误: {stderr_output}")
|
||||
|
||||
should_suppress_dialog = False
|
||||
if suppress_critical_dialog_on_stderr_match:
|
||||
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
||||
if suppress_critical_dialog_on_stderr_match in stderr_output:
|
||||
should_suppress_dialog = True
|
||||
elif isinstance(suppress_critical_dialog_on_stderr_match, (list, tuple)):
|
||||
for pattern in suppress_critical_dialog_on_stderr_match:
|
||||
if pattern in stderr_output:
|
||||
should_suppress_dialog = True
|
||||
break
|
||||
|
||||
if should_suppress_dialog:
|
||||
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
|
||||
else:
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
|
||||
|
||||
return False, e.stdout, stderr_output
|
||||
except FileNotFoundError:
|
||||
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
messagebox.showerror("错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||
return False, "", "Command not found."
|
||||
except Exception as e:
|
||||
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str} 发生未知错误: {e}")
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def _execute_interactive_mdadm_command(self, command_list, error_msg_prefix):
|
||||
"""专门处理 mdadm --create 命令的交互式执行"""
|
||||
if not PEXPECT_AVAILABLE:
|
||||
logger.warning("pexpect 模块未安装,将尝试使用 --run 参数避免交互")
|
||||
# 添加 --run 参数来避免交互式提示(自动回答 yes)
|
||||
if "--create" in command_list:
|
||||
# 在 --create 后面插入 --run
|
||||
create_idx = command_list.index("--create")
|
||||
command_list.insert(create_idx + 1, "--run")
|
||||
logger.info("已添加 --run 参数以自动确认创建")
|
||||
# 回退到普通命令执行
|
||||
return self._execute_shell_command(command_list, error_msg_prefix)
|
||||
|
||||
full_cmd_str = "sudo " + ' '.join(command_list)
|
||||
logger.info(f"执行交互式命令: {full_cmd_str}")
|
||||
|
||||
stdout_buffer = []
|
||||
|
||||
try:
|
||||
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300)
|
||||
child.logfile = sys.stdout
|
||||
|
||||
try:
|
||||
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
|
||||
if index == 0:
|
||||
stdout_buffer.append(child.before)
|
||||
password = simpledialog.askstring("Sudo 密码", "请输入您的 sudo 密码:", show='*')
|
||||
if not password:
|
||||
logger.error("sudo 密码未提供或取消。")
|
||||
messagebox.showerror("错误", "sudo 密码未提供或取消。")
|
||||
child.close(force=True)
|
||||
return False, "", "Sudo password not provided."
|
||||
child.sendline(password)
|
||||
try:
|
||||
auth_index = child.expect(['Authentication failure', pexpect.TIMEOUT], timeout=5)
|
||||
if auth_index == 0:
|
||||
logger.error("sudo 密码验证失败。")
|
||||
messagebox.showerror("错误", "sudo 密码验证失败。请检查密码并重试。")
|
||||
child.close(force=True)
|
||||
return False, "", "Sudo authentication failed."
|
||||
except pexpect.TIMEOUT:
|
||||
logger.info("sudo 密码验证成功或无需密码。继续...")
|
||||
except pexpect.TIMEOUT:
|
||||
logger.info("未在 5 秒内收到密码提示,假定命令无需密码或已在缓存中。继续...")
|
||||
|
||||
interaction_patterns = [
|
||||
r'Continue creating array.*\?',
|
||||
r'partition table exists.*Continue',
|
||||
r'write-indent bitmap.*enable it now.*\?',
|
||||
r'bitmap.*\[y/N\]\?',
|
||||
]
|
||||
|
||||
pattern_descriptions = {
|
||||
0: "mdadm 询问是否继续创建阵列(分区表将被清除)",
|
||||
1: "mdadm 提示分区表存在,询问是否继续",
|
||||
2: "mdadm 询问是否启用 write-intent bitmap",
|
||||
3: "mdadm 询问是否启用 bitmap",
|
||||
}
|
||||
|
||||
while True:
|
||||
try:
|
||||
index = child.expect(interaction_patterns + [pexpect.EOF, pexpect.TIMEOUT], timeout=300)
|
||||
|
||||
if index < len(interaction_patterns):
|
||||
# 匹配到交互式提示
|
||||
matched_text = child.after
|
||||
logger.info(f"检测到交互式提示 (index={index}): {matched_text}")
|
||||
|
||||
# 根据不同的提示给出不同的处理
|
||||
if index == 0 or index == 1:
|
||||
# 继续创建阵列(分区表相关)
|
||||
if messagebox.askyesno("mdadm 提示",
|
||||
"设备上存在分区表,创建 RAID 将清除分区表。\n"
|
||||
"是否继续创建 RAID 阵列?",
|
||||
default=messagebox.YES):
|
||||
child.sendline('y')
|
||||
logger.info("用户确认继续创建 RAID 阵列。")
|
||||
else:
|
||||
child.sendline('n')
|
||||
logger.info("用户选择不继续创建 RAID 阵列。中止操作。")
|
||||
return False, "", "User aborted RAID creation."
|
||||
elif index == 2 or index == 3:
|
||||
# bitmap 相关提示
|
||||
if messagebox.askyesno("mdadm 提示",
|
||||
"mdadm 建议启用 write-intent bitmap 以优化恢复速度。\n"
|
||||
"是否启用 bitmap?",
|
||||
default=messagebox.NO):
|
||||
child.sendline('y')
|
||||
logger.info("用户确认启用 bitmap。")
|
||||
else:
|
||||
child.sendline('n')
|
||||
logger.info("用户选择不启用 bitmap,继续创建阵列。")
|
||||
elif index == len(interaction_patterns):
|
||||
# EOF
|
||||
break
|
||||
elif index == len(interaction_patterns) + 1:
|
||||
# TIMEOUT
|
||||
logger.warning(f"等待 mdadm 命令输出时超时。已收集输出:\n{''.join(stdout_buffer)}")
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: mdadm 命令在交互完成后未正常退出,等待 EOF 超时。")
|
||||
return False, "".join(stdout_buffer), "Timeout waiting for EOF."
|
||||
|
||||
except pexpect.exceptions.EOF:
|
||||
break
|
||||
|
||||
final_output = child.before
|
||||
stdout_buffer.append(final_output)
|
||||
|
||||
child.close()
|
||||
if child.exitstatus == 0:
|
||||
logger.info(f"交互式命令成功完成: {full_cmd_str}")
|
||||
return True, "".join(stdout_buffer), ""
|
||||
else:
|
||||
logger.error(f"交互式命令失败,退出码: {child.exitstatus}")
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
|
||||
return False, "".join(stdout_buffer), f"Command failed with exit code {child.exitstatus}"
|
||||
|
||||
except pexpect.exceptions.ExceptionPexpect as e:
|
||||
logger.error(f"交互式命令执行过程中发生 pexpect 错误: {e}")
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: pexpect 内部错误: {e}")
|
||||
return False, "", str(e)
|
||||
except Exception as e:
|
||||
logger.error(f"交互式命令执行时发生未知错误: {e}")
|
||||
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def create_raid_array(self, devices, level, chunk_size):
|
||||
"""创建 RAID 阵列"""
|
||||
if not isinstance(devices, list):
|
||||
logger.error(f"传入的设备列表类型不是 list: {type(devices)}")
|
||||
messagebox.showerror("错误", f"不支持的 RAID 级别类型: {type(level)}")
|
||||
return False
|
||||
|
||||
if not devices:
|
||||
messagebox.showerror("错误", "创建 RAID 阵列至少需要一个设备(RAID0)。")
|
||||
return False
|
||||
|
||||
num_devices = len(devices)
|
||||
|
||||
if level == "1" and num_devices < 2:
|
||||
messagebox.showerror("错误", "RAID1 至少需要两个设备。")
|
||||
return False
|
||||
elif level == "5" and num_devices < 3:
|
||||
messagebox.showerror("错误", "RAID5 至少需要三个设备。")
|
||||
return False
|
||||
elif level == "6" and num_devices < 4:
|
||||
messagebox.showerror("错误", "RAID6 至少需要四个设备。")
|
||||
return False
|
||||
elif level == "10" and num_devices < 2:
|
||||
messagebox.showerror("错误", "RAID10 至少需要两个设备。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认创建 RAID 阵列",
|
||||
f"您确定要创建 RAID {level} 阵列吗?\n"
|
||||
f"这将使用设备: {', '.join(devices)}。\n"
|
||||
f"此操作会擦除这些设备上的数据!",
|
||||
default=messagebox.NO):
|
||||
logger.info("用户取消了创建 RAID 阵列的操作。")
|
||||
return False
|
||||
|
||||
array_name = None
|
||||
try:
|
||||
mdadm_detail_result = self.system_manager._run_command(["mdadm", "--detail", "--scan"], root_privilege=True)
|
||||
existing_arrays = re.findall(r'ARRAY\s+/dev/(md\d+)', mdadm_detail_result[0])
|
||||
if existing_arrays:
|
||||
max_num = max([int(re.search(r'\d+', name).group()) for name in existing_arrays])
|
||||
array_name = f"/dev/md{max_num + 1}"
|
||||
else:
|
||||
array_name = "/dev/md0"
|
||||
except Exception as e:
|
||||
logger.warning(f"检测现有 RAID 阵列时出错: {e},将默认使用 /dev/md0。")
|
||||
array_name = "/dev/md0"
|
||||
|
||||
create_cmd = [
|
||||
"mdadm", "--create", array_name, "--level=" + level,
|
||||
"--raid-devices=" + str(num_devices)
|
||||
]
|
||||
if chunk_size:
|
||||
create_cmd.append("--chunk=" + str(chunk_size))
|
||||
create_cmd.extend(devices)
|
||||
|
||||
logger.info(f"尝试创建 RAID {level} 阵列 {array_name},使用设备: {devices}")
|
||||
|
||||
success, stdout, stderr = self._execute_interactive_mdadm_command(create_cmd, f"创建 RAID {level} 阵列失败")
|
||||
|
||||
if success:
|
||||
if "already exists" in stderr.lower() or "already exists" in stdout.lower():
|
||||
messagebox.showerror("错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
|
||||
return False
|
||||
|
||||
messagebox.showinfo("成功", f"成功创建 RAID {level} 阵列 {array_name}。")
|
||||
|
||||
try:
|
||||
mdadm_conf_path = "/etc/mdadm.conf"
|
||||
success_scan, stdout_scan, stderr_scan = self._execute_shell_command(
|
||||
["mdadm", "--detail", "--scan"],
|
||||
"生成 mdadm.conf 扫描信息失败",
|
||||
suppress_critical_dialog_on_stderr_match="No arrays"
|
||||
)
|
||||
|
||||
if success_scan:
|
||||
success_mkdir, _, _ = self._execute_shell_command(
|
||||
["mkdir", "-p", "/etc"],
|
||||
"创建 /etc 目录失败"
|
||||
)
|
||||
if not success_mkdir:
|
||||
messagebox.showerror("错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
||||
return False
|
||||
|
||||
success_conf, _, stderr_conf = self._execute_shell_command(
|
||||
["sh", "-c", f"echo '{stdout_scan.strip()}' >> {mdadm_conf_path}"],
|
||||
f"更新 {mdadm_conf_path} 失败"
|
||||
)
|
||||
if success_conf:
|
||||
logger.info(f"已将新的 RAID 阵列信息追加到 {mdadm_conf_path}。")
|
||||
else:
|
||||
logger.warning(f"无法追加到 {mdadm_conf_path}。这可能需要手动修复。错误: {stderr_conf}")
|
||||
|
||||
else:
|
||||
logger.warning("mdadm --detail --scan 失败,无法自动更新 mdadm.conf。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新 mdadm.conf 时发生错误: {e}")
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_raid_array(self, array_path):
|
||||
"""停止 RAID 阵列"""
|
||||
if not messagebox.askyesno("确认停止 RAID 阵列",
|
||||
f"您确定要停止 RAID 阵列 {array_path} 吗?\n"
|
||||
f"这将卸载该阵列(如果已挂载)并将其置为停止状态。",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了停止 RAID 阵列 {array_path} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试停止 RAID 阵列 {array_path}。")
|
||||
|
||||
try:
|
||||
stdout, stderr = self.system_manager._run_command(
|
||||
["mdadm", "--stop", array_path],
|
||||
root_privilege=True, check_output=True
|
||||
)
|
||||
messagebox.showinfo("成功", f"成功停止 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr.strip() if e.stderr else str(e)
|
||||
logger.error(f"停止 RAID 阵列 {array_path} 失败: {error_msg}")
|
||||
messagebox.showerror("错误", f"停止 RAID 阵列 {array_path} 失败。\n错误: {error_msg}")
|
||||
return False
|
||||
|
||||
def delete_active_raid_array(self, array_path, member_devices, array_uuid):
|
||||
"""删除活动的 RAID 阵列"""
|
||||
if not messagebox.askyesno("确认删除 RAID 阵列",
|
||||
f"您确定要删除 RAID 阵列 {array_path} 吗?\n"
|
||||
f"此操作将停止阵列并清除所有成员设备的超级块!",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除 RAID 阵列 {array_path} 的操作。")
|
||||
return False
|
||||
|
||||
if not self.stop_raid_array(array_path):
|
||||
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
|
||||
return False
|
||||
|
||||
all_wiped_successfully = True
|
||||
if member_devices:
|
||||
for dev in member_devices:
|
||||
if dev:
|
||||
logger.info(f"尝试清除设备 {dev} 上的 RAID 超级块。")
|
||||
wipe_success, _, wipe_stderr = self._execute_shell_command(
|
||||
["mdadm", "--zero-superblock", dev],
|
||||
f"清除设备 {dev} 的 RAID 超级块失败"
|
||||
)
|
||||
if not wipe_success:
|
||||
logger.error(f"清除设备 {dev} 的 RAID 超级块失败: {wipe_stderr}")
|
||||
all_wiped_successfully = False
|
||||
|
||||
try:
|
||||
mdadm_conf_path = "/etc/mdadm.conf"
|
||||
if array_uuid:
|
||||
success_conf_rm, _, stderr_conf_rm = self._execute_shell_command(
|
||||
["sed", "-i", f"/{re.escape(array_uuid)}/d", mdadm_conf_path],
|
||||
f"从 {mdadm_conf_path} 中删除 RAID 条目失败",
|
||||
suppress_critical_dialog_on_stderr_match=("No such file", "没有那个文件")
|
||||
)
|
||||
if not success_conf_rm:
|
||||
logger.warning(f"无法从 {mdadm_conf_path} 中删除条目: {stderr_conf_rm}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新 {mdadm_conf_path} 时发生错误: {e}")
|
||||
|
||||
if all_wiped_successfully:
|
||||
messagebox.showinfo("成功", f"成功删除 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
else:
|
||||
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
|
||||
return False
|
||||
|
||||
def delete_configured_raid_array(self, uuid):
|
||||
"""删除停止状态 RAID 阵列的配置文件条目"""
|
||||
if not messagebox.askyesno("确认删除 RAID 阵列配置",
|
||||
f"您确定要删除 UUID 为 {uuid} 的 RAID 阵列配置吗?\n"
|
||||
f"这将从 mdadm.conf 中删除该阵列的配置。",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了删除 RAID 阵列配置 (UUID: {uuid}) 的操作。")
|
||||
return False
|
||||
|
||||
mdadm_conf_path = "/etc/mdadm.conf"
|
||||
success, _, stderr = self._execute_shell_command(
|
||||
["sed", "-i", f"/{re.escape(uuid)}/d", mdadm_conf_path],
|
||||
f"从 {mdadm_conf_path} 中删除 RAID 条目失败"
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"成功删除 UUID 为 {uuid} 的 RAID 阵列配置。")
|
||||
return True
|
||||
else:
|
||||
messagebox.showerror("错误", f"删除 RAID 阵列配置失败: {e}")
|
||||
return False
|
||||
|
||||
def activate_raid_array(self, array_path, array_uuid):
|
||||
"""激活已停止的 RAID 阵列"""
|
||||
if not array_uuid or array_uuid == 'N/A':
|
||||
messagebox.showerror("错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
|
||||
return False
|
||||
|
||||
if not messagebox.askyesno("确认激活 RAID 阵列",
|
||||
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
|
||||
"这将尝试重新组装阵列。",
|
||||
default=messagebox.NO):
|
||||
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试激活 RAID 阵列 {array_path} (UUID: {array_uuid})。")
|
||||
success, stdout, stderr = self._execute_shell_command(
|
||||
["mdadm", "--assemble", "--uuid", array_uuid, array_path],
|
||||
f"激活 RAID 阵列 {array_path} 失败"
|
||||
)
|
||||
if success:
|
||||
messagebox.showinfo("成功", f"成功激活 RAID 阵列 {array_path}。")
|
||||
return True
|
||||
return False
|
||||
@@ -1 +1,17 @@
|
||||
PySide6
|
||||
# Linux 存储管理工具 - Tkinter 版本
|
||||
# 核心依赖(Python 内置):
|
||||
# - tkinter (GUI)
|
||||
# - threading (后台任务)
|
||||
# - queue (线程通信)
|
||||
|
||||
# 可选依赖:
|
||||
# pexpect - 用于交互式命令(如 RAID 创建时的密码输入)
|
||||
# 安装: pip3 install pexpect
|
||||
|
||||
# CentOS 8 安装命令:
|
||||
# sudo yum install python3 python3-tkinter parted mdadm lvm2
|
||||
# sudo pip3 install pexpect
|
||||
|
||||
# Ubuntu/Debian 安装命令:
|
||||
# sudo apt-get install python3 python3-tk parted mdadm lvm2
|
||||
# sudo pip3 install pexpect
|
||||
|
||||
41
run_tkinter.sh
Normal file
41
run_tkinter.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Linux 存储管理工具 - Tkinter 版本启动脚本
|
||||
# 适配低版本系统(如 CentOS 8)
|
||||
|
||||
# 检查 Python 版本
|
||||
python3 --version > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: 未找到 Python3,请安装 Python 3.6 或更高版本。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 tkinter
|
||||
python3 -c "import tkinter" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: Python 未安装 tkinter 模块。"
|
||||
echo "请安装 tkinter:"
|
||||
echo " CentOS/RHEL: sudo yum install python3-tkinter"
|
||||
echo " Ubuntu/Debian: sudo apt-get install python3-tk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查必要的系统工具
|
||||
for tool in lsblk parted mdadm lvm; do
|
||||
which $tool > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "警告: 未找到工具 '$tool',某些功能可能无法使用。"
|
||||
fi
|
||||
done
|
||||
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 以 root 权限运行(存储管理需要)
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "注意: 存储管理工具需要 root 权限才能正常工作。"
|
||||
echo "正在尝试使用 sudo 启动..."
|
||||
sudo python3 "${SCRIPT_DIR}/main_tkinter.py" "$@"
|
||||
else
|
||||
python3 "${SCRIPT_DIR}/main_tkinter.py" "$@"
|
||||
fi
|
||||
106
verify_tkinter.py
Normal file
106
verify_tkinter.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
验证 Tkinter 版本的存储管理工具是否可以正确导入
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def check_python_version():
|
||||
"""检查 Python 版本"""
|
||||
print(f"Python 版本: {sys.version}")
|
||||
if sys.version_info < (3, 6):
|
||||
print("错误: 需要 Python 3.6 或更高版本")
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_tkinter():
|
||||
"""检查 tkinter 是否可用"""
|
||||
try:
|
||||
import tkinter as tk
|
||||
print(f"Tkinter 版本: {tk.Tcl().eval('info patch')}")
|
||||
return True
|
||||
except ImportError:
|
||||
print("错误: 未找到 tkinter 模块")
|
||||
print("请安装 tkinter:")
|
||||
print(" CentOS/RHEL: sudo yum install python3-tkinter")
|
||||
print(" Ubuntu/Debian: sudo apt-get install python3-tk")
|
||||
return False
|
||||
|
||||
def check_modules():
|
||||
"""检查自定义模块"""
|
||||
modules_to_check = [
|
||||
('logger_config_tkinter', '日志配置'),
|
||||
('dialogs_tkinter', '对话框'),
|
||||
('disk_operations_tkinter', '磁盘操作'),
|
||||
('raid_operations_tkinter', 'RAID 操作'),
|
||||
('lvm_operations_tkinter', 'LVM 操作'),
|
||||
('occupation_resolver_tkinter', '设备占用解除'),
|
||||
('mainwindow_tkinter', '主窗口'),
|
||||
]
|
||||
|
||||
all_ok = True
|
||||
for module_name, desc in modules_to_check:
|
||||
try:
|
||||
__import__(module_name)
|
||||
print(f"✓ {desc}模块 ({module_name}) 加载成功")
|
||||
except Exception as e:
|
||||
print(f"✗ {desc}模块 ({module_name}) 加载失败: {e}")
|
||||
all_ok = False
|
||||
|
||||
return all_ok
|
||||
|
||||
def check_system_modules():
|
||||
"""检查系统信息模块"""
|
||||
try:
|
||||
from system_info import SystemInfoManager
|
||||
print("✓ 系统信息模块 (system_info) 加载成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ 系统信息模块 (system_info) 加载失败: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=" * 60)
|
||||
print("Linux 存储管理工具 - Tkinter 版本验证")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
checks = [
|
||||
("Python 版本", check_python_version),
|
||||
("Tkinter 模块", check_tkinter),
|
||||
("系统模块", check_system_modules),
|
||||
("自定义模块", check_modules),
|
||||
]
|
||||
|
||||
results = []
|
||||
for name, check_func in checks:
|
||||
print(f"\n检查 {name}...")
|
||||
print("-" * 40)
|
||||
result = check_func()
|
||||
results.append((name, result))
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("验证结果汇总")
|
||||
print("=" * 60)
|
||||
|
||||
all_passed = True
|
||||
for name, result in results:
|
||||
status = "通过" if result else "失败"
|
||||
symbol = "✓" if result else "✗"
|
||||
print(f"{symbol} {name}: {status}")
|
||||
if not result:
|
||||
all_passed = False
|
||||
|
||||
print()
|
||||
if all_passed:
|
||||
print("✓ 所有检查通过!可以运行 main_tkinter.py 启动程序。")
|
||||
return 0
|
||||
else:
|
||||
print("✗ 部分检查失败,请根据上述提示修复问题。")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user