1409 lines
66 KiB
Python
1409 lines
66 KiB
Python
# mainwindow_tkinter.py
|
|
import sys
|
|
import logging
|
|
import re
|
|
import os
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, Menu
|
|
from tkinter.scrolledtext import ScrolledText
|
|
|
|
# 导入系统信息管理模块
|
|
from system_info import SystemInfoManager
|
|
# 导入日志配置
|
|
from logger_config_tkinter import setup_logging, logger
|
|
# 导入磁盘操作模块
|
|
from disk_operations_tkinter import DiskOperations
|
|
# 导入 RAID 操作模块
|
|
from raid_operations_tkinter import RaidOperations
|
|
# 导入 LVM 操作模块
|
|
from lvm_operations_tkinter import LvmOperations
|
|
# 导入新的 resolver 类
|
|
from occupation_resolver_tkinter import OccupationResolver
|
|
# 导入自定义对话框
|
|
from dialogs_tkinter import (CreatePartitionDialog, MountDialog, CreateRaidDialog,
|
|
CreatePvDialog, CreateVgDialog, CreateLvDialog)
|
|
# 导入 SMART 对话框
|
|
from dialogs_smart import SmartInfoDialog, SmartOverviewDialog
|
|
from smart_monitor import SmartMonitor
|
|
# 导入增强对话框
|
|
from dialogs_enhanced import (EnhancedFormatDialog, EnhancedRaidDeleteDialog,
|
|
EnhancedPartitionDialog, DependencyCheckDialog)
|
|
|
|
|
|
class MainWindow:
|
|
"""主窗口类 - Tkinter 版本"""
|
|
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Linux 存储管理工具")
|
|
self.root.geometry("1200x800")
|
|
|
|
# 创建主框架
|
|
self.main_frame = ttk.Frame(self.root, padding="5")
|
|
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 初始化管理器和操作类
|
|
self.system_manager = SystemInfoManager()
|
|
self.lvm_ops = LvmOperations()
|
|
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops)
|
|
self.raid_ops = RaidOperations(self.system_manager)
|
|
|
|
# 实例化 OccupationResolver
|
|
self.occupation_resolver = OccupationResolver(
|
|
shell_executor_func=self.lvm_ops._execute_shell_command,
|
|
mount_info_getter_func=self.system_manager.get_mountpoint_for_device
|
|
)
|
|
|
|
# 当前右键菜单引用(用于自动关闭)
|
|
self._current_context_menu = None
|
|
self._menu_close_binding = None
|
|
|
|
# 创建界面
|
|
self._create_widgets()
|
|
|
|
# 设置日志
|
|
setup_logging(self.log_text)
|
|
logger.info("应用程序启动。")
|
|
|
|
# 连接磁盘操作的回调
|
|
self.disk_ops.set_formatting_callback(self.on_disk_formatting_finished)
|
|
|
|
# 启动队列检查
|
|
self.disk_ops.start_queue_check(self.root)
|
|
|
|
# 初始化时刷新所有数据
|
|
self.refresh_all_info()
|
|
logger.info("所有设备信息已初始化加载。")
|
|
|
|
def _create_widgets(self):
|
|
"""创建界面元素"""
|
|
# 顶部按钮栏
|
|
btn_frame = ttk.Frame(self.main_frame)
|
|
btn_frame.pack(fill=tk.X, pady=(0, 5))
|
|
|
|
# 刷新按钮
|
|
self.refresh_btn = ttk.Button(btn_frame, text="刷新数据", command=self.refresh_all_info)
|
|
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
# SMART 监控按钮
|
|
self.smart_btn = ttk.Button(btn_frame, text="SMART 监控", command=self._show_smart_overview)
|
|
self.smart_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
# 菜单栏
|
|
self._create_menu()
|
|
|
|
# Notebook (Tab 控件)
|
|
self.notebook = ttk.Notebook(self.main_frame)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# === 块设备 Tab ===
|
|
self.tab_block = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.tab_block, text="块设备概览")
|
|
|
|
# 块设备 Treeview
|
|
self._create_block_devices_tree()
|
|
|
|
# === RAID 管理 Tab ===
|
|
self.tab_raid = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.tab_raid, text="RAID 管理")
|
|
|
|
# RAID Treeview
|
|
self._create_raid_tree()
|
|
|
|
# === LVM 管理 Tab ===
|
|
self.tab_lvm = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.tab_lvm, text="LVM 管理")
|
|
|
|
# LVM Treeview
|
|
self._create_lvm_tree()
|
|
|
|
# 日志区域
|
|
log_frame = ttk.LabelFrame(self.main_frame, text="日志输出", padding="5")
|
|
log_frame.pack(fill=tk.X, pady=(5, 0))
|
|
|
|
self.log_text = ScrolledText(log_frame, height=8, state=tk.DISABLED)
|
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
|
|
|
def _create_menu(self):
|
|
"""创建菜单栏"""
|
|
menubar = tk.Menu(self.root)
|
|
self.root.config(menu=menubar)
|
|
|
|
# 工具菜单
|
|
tools_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="工具", menu=tools_menu)
|
|
tools_menu.add_command(label="SMART 监控", command=self._show_smart_overview)
|
|
tools_menu.add_separator()
|
|
tools_menu.add_command(label="刷新数据", command=self.refresh_all_info)
|
|
|
|
# 帮助菜单
|
|
help_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="帮助", menu=help_menu)
|
|
help_menu.add_command(label="依赖检查", command=self._show_dependency_check)
|
|
help_menu.add_separator()
|
|
help_menu.add_command(label="关于", command=self._show_about)
|
|
|
|
def _show_about(self):
|
|
"""显示关于对话框"""
|
|
messagebox.showinfo(
|
|
"关于",
|
|
"Linux 存储管理工具\n\n"
|
|
"版本: 2.0 (Tkinter 版)\n"
|
|
"适配低版本系统 (CentOS 8+)\n\n"
|
|
"功能:\n"
|
|
"- 块设备管理\n"
|
|
"- RAID 管理\n"
|
|
"- LVM 管理\n"
|
|
"- SMART 监控\n\n"
|
|
"使用 Python 3.6+ 和 Tkinter 构建"
|
|
)
|
|
|
|
def _show_dependency_check(self):
|
|
"""显示依赖检查对话框"""
|
|
DependencyCheckDialog(self.root)
|
|
|
|
def _create_block_devices_tree(self):
|
|
"""创建块设备 Treeview"""
|
|
# 创建框架
|
|
tree_frame = ttk.Frame(self.tab_block)
|
|
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 定义列
|
|
self.block_columns = [
|
|
("设备名", 'name'), ("类型", 'type'), ("大小", 'size'), ("挂载点", 'mountpoint'),
|
|
("文件系统", 'fstype'), ("只读", 'ro'), ("UUID", 'uuid'), ("PARTUUID", 'partuuid'),
|
|
("厂商", 'vendor'), ("型号", 'model'), ("序列号", 'serial'),
|
|
("主次号", 'maj:min'), ("父设备名", 'pkname'),
|
|
]
|
|
|
|
# 创建 Treeview
|
|
self.tree_block = ttk.Treeview(tree_frame, columns=[c[1] for c in self.block_columns],
|
|
show='tree headings')
|
|
|
|
# 设置表头
|
|
self.tree_block.heading('#0', text='设备结构')
|
|
self.tree_block.column('#0', width=150)
|
|
|
|
for col_name, col_id in self.block_columns:
|
|
self.tree_block.heading(col_id, text=col_name)
|
|
self.tree_block.column(col_id, width=80, anchor=tk.W)
|
|
|
|
# 滚动条
|
|
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_block.yview)
|
|
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_block.xview)
|
|
self.tree_block.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
|
|
|
# 布局
|
|
self.tree_block.grid(row=0, column=0, sticky='nsew')
|
|
vsb.grid(row=0, column=1, sticky='ns')
|
|
hsb.grid(row=1, column=0, sticky='ew')
|
|
tree_frame.grid_rowconfigure(0, weight=1)
|
|
tree_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# 右键菜单绑定
|
|
self.tree_block.bind("<Button-3>", self._on_block_device_right_click)
|
|
|
|
def _create_raid_tree(self):
|
|
"""创建 RAID Treeview"""
|
|
tree_frame = ttk.Frame(self.tab_raid)
|
|
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 定义列
|
|
self.raid_columns = [
|
|
("阵列设备", 'device'), ("级别", 'level'), ("状态", 'state'), ("大小", 'array_size'),
|
|
("活动设备", 'active_devices'), ("失败设备", 'failed_devices'),
|
|
("备用设备", 'spare_devices'), ("总设备数", 'total_devices'),
|
|
("UUID", 'uuid'), ("名称", 'name'), ("Chunk Size", 'chunk_size'), ("挂载点", 'mountpoint')
|
|
]
|
|
|
|
# 创建 Treeview
|
|
self.tree_raid = ttk.Treeview(tree_frame, columns=[c[1] for c in self.raid_columns],
|
|
show='tree headings')
|
|
|
|
# 设置表头
|
|
self.tree_raid.heading('#0', text='RAID 结构')
|
|
self.tree_raid.column('#0', width=150)
|
|
|
|
for col_name, col_id in self.raid_columns:
|
|
self.tree_raid.heading(col_id, text=col_name)
|
|
self.tree_raid.column(col_id, width=80, anchor=tk.W)
|
|
|
|
# 滚动条
|
|
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_raid.yview)
|
|
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_raid.xview)
|
|
self.tree_raid.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
|
|
|
# 布局
|
|
self.tree_raid.grid(row=0, column=0, sticky='nsew')
|
|
vsb.grid(row=0, column=1, sticky='ns')
|
|
hsb.grid(row=1, column=0, sticky='ew')
|
|
tree_frame.grid_rowconfigure(0, weight=1)
|
|
tree_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# 右键菜单绑定
|
|
self.tree_raid.bind("<Button-3>", self._on_raid_right_click)
|
|
|
|
def _create_lvm_tree(self):
|
|
"""创建 LVM Treeview"""
|
|
tree_frame = ttk.Frame(self.tab_lvm)
|
|
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 定义列
|
|
self.lvm_columns = [
|
|
("名称", 'name'), ("大小", 'size'), ("属性", 'attr'), ("UUID", 'uuid'),
|
|
("关联", 'relation'), ("空闲/已用", 'usage'), ("路径/格式", 'path_fmt'), ("挂载点", 'mountpoint')
|
|
]
|
|
|
|
# 创建 Treeview
|
|
self.tree_lvm = ttk.Treeview(tree_frame, columns=[c[1] for c in self.lvm_columns],
|
|
show='tree headings')
|
|
|
|
# 设置表头
|
|
self.tree_lvm.heading('#0', text='LVM 结构')
|
|
self.tree_lvm.column('#0', width=150)
|
|
|
|
for col_name, col_id in self.lvm_columns:
|
|
self.tree_lvm.heading(col_id, text=col_name)
|
|
self.tree_lvm.column(col_id, width=80, anchor=tk.W)
|
|
|
|
# 滚动条
|
|
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_lvm.yview)
|
|
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_lvm.xview)
|
|
self.tree_lvm.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
|
|
|
# 布局
|
|
self.tree_lvm.grid(row=0, column=0, sticky='nsew')
|
|
vsb.grid(row=0, column=1, sticky='ns')
|
|
hsb.grid(row=1, column=0, sticky='ew')
|
|
tree_frame.grid_rowconfigure(0, weight=1)
|
|
tree_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# 右键菜单绑定
|
|
self.tree_lvm.bind("<Button-3>", self._on_lvm_right_click)
|
|
|
|
def refresh_all_info(self):
|
|
"""刷新所有设备信息"""
|
|
logger.info("开始刷新所有设备信息...")
|
|
self.refresh_block_devices_info()
|
|
self.refresh_raid_info()
|
|
self.refresh_lvm_info()
|
|
logger.info("所有设备信息刷新完成。")
|
|
|
|
def refresh_block_devices_info(self):
|
|
"""刷新块设备信息"""
|
|
# 清空树
|
|
for item in self.tree_block.get_children():
|
|
self.tree_block.delete(item)
|
|
|
|
try:
|
|
devices = self.system_manager.get_block_devices()
|
|
for dev in devices:
|
|
self._add_device_to_tree(self.tree_block, '', dev)
|
|
|
|
logger.info("块设备信息刷新成功。")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"刷新块设备信息失败: {e}")
|
|
logger.error(f"刷新块设备信息失败: {e}")
|
|
|
|
def _add_device_to_tree(self, tree, parent, dev_data):
|
|
"""添加设备到树"""
|
|
# 获取显示值
|
|
values = []
|
|
for col_name, col_id in self.block_columns:
|
|
value = dev_data.get(col_id)
|
|
if col_id == 'ro':
|
|
value = "是" if value else "否"
|
|
elif value is None:
|
|
value = ""
|
|
values.append(value)
|
|
|
|
# 插入节点
|
|
item_id = tree.insert(parent, tk.END, text=dev_data.get('name', ''), values=values)
|
|
|
|
# 保存完整数据
|
|
tree.item(item_id, tags=(str(dev_data),))
|
|
|
|
# 递归添加子设备
|
|
if 'children' in dev_data:
|
|
for child in dev_data['children']:
|
|
self._add_device_to_tree(tree, item_id, child)
|
|
tree.item(item_id, open=True)
|
|
|
|
def _get_item_data(self, tree, item_id):
|
|
"""获取树项关联的数据"""
|
|
tags = tree.item(item_id, 'tags')
|
|
if tags:
|
|
try:
|
|
# 解析字符串为字典
|
|
return eval(tags[0])
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def _close_context_menu(self):
|
|
"""关闭当前右键菜单"""
|
|
if self._current_context_menu is not None:
|
|
try:
|
|
self._current_context_menu.unpost()
|
|
except:
|
|
pass
|
|
self._current_context_menu = None
|
|
# 解绑全局点击事件
|
|
if self._menu_close_binding is not None:
|
|
try:
|
|
self.root.unbind('<Button-1>', self._menu_close_binding)
|
|
self._menu_close_binding = None
|
|
except:
|
|
pass
|
|
|
|
def _bind_menu_close(self):
|
|
"""绑定点击事件来关闭菜单"""
|
|
if self._current_context_menu is not None:
|
|
self._menu_close_binding = self.root.bind('<Button-1>', self._on_click_outside_menu)
|
|
|
|
def _on_click_outside_menu(self, event):
|
|
"""点击菜单外部时关闭菜单"""
|
|
# tk.Menu 会拦截自己的点击事件,所以如果这里被调用,说明点击在菜单外部
|
|
self._close_context_menu()
|
|
|
|
def _show_context_menu(self, menu, x, y):
|
|
"""显示右键菜单并记录引用"""
|
|
# 先关闭旧菜单
|
|
self._close_context_menu()
|
|
# 记录新菜单
|
|
self._current_context_menu = menu
|
|
# 显示菜单
|
|
menu.post(x, y)
|
|
# 绑定全局点击事件来关闭菜单(延迟绑定避免立即触发)
|
|
self.root.after(100, self._bind_menu_close)
|
|
|
|
# === 块设备右键菜单 ===
|
|
def _on_block_device_right_click(self, event):
|
|
"""块设备右键点击事件"""
|
|
# 先关闭任何已打开的菜单
|
|
self._close_context_menu()
|
|
|
|
item = self.tree_block.identify_row(event.y)
|
|
if item:
|
|
self.tree_block.selection_set(item)
|
|
else:
|
|
return
|
|
|
|
# 创建菜单
|
|
menu = tk.Menu(self.root, tearoff=0)
|
|
|
|
# 创建子菜单
|
|
create_menu = tk.Menu(menu, tearoff=0)
|
|
create_menu.add_command(label="创建 RAID 阵列...", command=self._handle_create_raid_array)
|
|
create_menu.add_command(label="创建物理卷 (PV)...", command=self._handle_create_pv)
|
|
menu.add_cascade(label="创建...", menu=create_menu)
|
|
menu.add_separator()
|
|
|
|
if item:
|
|
dev_data = self._get_item_data(self.tree_block, item)
|
|
if dev_data:
|
|
device_name = dev_data.get('name')
|
|
device_type = dev_data.get('type')
|
|
mount_point = dev_data.get('mountpoint')
|
|
device_path = dev_data.get('path', f"/dev/{device_name}")
|
|
|
|
# 磁盘操作
|
|
if device_type == 'disk':
|
|
menu.add_command(label=f"创建分区 {device_path}...",
|
|
command=lambda: self._handle_create_partition(device_path, dev_data))
|
|
menu.add_command(label=f"擦除分区表 {device_path}...",
|
|
command=lambda: self._handle_wipe_partition_table(device_path))
|
|
menu.add_separator()
|
|
|
|
# 检查磁盘状态
|
|
has_children = dev_data.get('children') is not None and len(dev_data.get('children', [])) > 0
|
|
fstype = dev_data.get('fstype', '')
|
|
|
|
# 如果磁盘有文件系统(直接格式化的物理盘),添加挂载/卸载/格式化操作
|
|
if fstype and fstype not in ['LVM2_member', 'linux_raid_member']:
|
|
if not mount_point or mount_point == '' or mount_point == 'N/A':
|
|
menu.add_command(label=f"挂载 {device_path}...",
|
|
command=lambda: self._handle_mount(device_path))
|
|
else:
|
|
menu.add_command(label=f"卸载 {device_path}",
|
|
command=lambda: self._unmount_and_refresh(device_path))
|
|
menu.add_separator()
|
|
|
|
# 格式化物理盘
|
|
menu.add_command(label=f"格式化物理盘 {device_path}...",
|
|
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
|
|
|
|
# 文件系统工具
|
|
fs_menu = tk.Menu(menu, tearoff=0)
|
|
if fstype.startswith('ext'):
|
|
fs_menu.add_command(label=f"检查文件系统 (fsck) {device_path}",
|
|
command=lambda dp=device_path, ft=fstype: self._handle_fsck(dp, ft))
|
|
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {device_path}",
|
|
command=lambda dp=device_path, ft=fstype: self._handle_resize2fs(dp, ft))
|
|
elif fstype == 'xfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {device_path}",
|
|
command=lambda dp=device_path: self._handle_xfs_repair(dp))
|
|
elif fstype.lower() == 'ntfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (ntfsfix) {device_path}",
|
|
command=lambda dp=device_path: self._handle_ntfsfix(dp))
|
|
if fs_menu.index(tk.END) is not None:
|
|
menu.add_cascade(label=f"文件系统工具 {device_path}", menu=fs_menu)
|
|
|
|
menu.add_separator()
|
|
elif not has_children:
|
|
# 没有文件系统、没有子分区的空物理盘,可以直接格式化
|
|
menu.add_command(label=f"直接格式化物理盘 {device_path}...",
|
|
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
|
|
menu.add_separator()
|
|
|
|
# SMART 信息
|
|
menu.add_command(label=f"查看 SMART 信息 {device_path}",
|
|
command=lambda: self._handle_show_smart(device_path))
|
|
menu.add_separator()
|
|
|
|
# 分区操作
|
|
if device_type == 'part':
|
|
if not mount_point or mount_point == '' or mount_point == 'N/A':
|
|
menu.add_command(label=f"挂载 {device_path}...",
|
|
command=lambda: self._handle_mount(device_path))
|
|
elif mount_point != '[SWAP]':
|
|
menu.add_command(label=f"卸载 {device_path}",
|
|
command=lambda: self._unmount_and_refresh(device_path))
|
|
menu.add_separator()
|
|
|
|
menu.add_command(label=f"删除分区 {device_path}",
|
|
command=lambda: self._handle_delete_partition(device_path))
|
|
menu.add_command(label=f"格式化分区 {device_path}...",
|
|
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
|
|
|
|
# 文件系统检查和修复
|
|
fstype = dev_data.get('fstype', '')
|
|
if fstype:
|
|
fs_menu = tk.Menu(menu, tearoff=0)
|
|
# 根据文件系统类型添加不同的检查/修复选项
|
|
if fstype.startswith('ext'):
|
|
fs_menu.add_command(label=f"检查文件系统 (fsck) {device_path}",
|
|
command=lambda dp=device_path, ft=fstype: self._handle_fsck(dp, ft))
|
|
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {device_path}",
|
|
command=lambda dp=device_path, ft=fstype: self._handle_resize2fs(dp, ft))
|
|
elif fstype == 'xfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {device_path}",
|
|
command=lambda dp=device_path: self._handle_xfs_repair(dp))
|
|
elif fstype.lower() == 'ntfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (ntfsfix) {device_path}",
|
|
command=lambda dp=device_path: self._handle_ntfsfix(dp))
|
|
if fs_menu.index(tk.END) is not None:
|
|
menu.add_cascade(label=f"文件系统工具 {device_path}", menu=fs_menu)
|
|
|
|
menu.add_separator()
|
|
|
|
# Loop 设备操作
|
|
if device_type == 'loop':
|
|
if not mount_point or mount_point == '' or mount_point == 'N/A':
|
|
menu.add_command(label=f"挂载 {device_path}...",
|
|
command=lambda: self._handle_mount(device_path))
|
|
else:
|
|
menu.add_command(label=f"卸载 {device_path}",
|
|
command=lambda: self._unmount_and_refresh(device_path))
|
|
menu.add_separator()
|
|
|
|
# 解除占用
|
|
if device_type in ['disk', 'part', 'loop']:
|
|
menu.add_command(label=f"解除占用 {device_path}",
|
|
command=lambda: self._handle_resolve_device_occupation(device_path))
|
|
|
|
# 关闭交换分区
|
|
if device_type == 'part' and mount_point == '[SWAP]':
|
|
menu.add_command(label=f"关闭交换分区 {device_path}",
|
|
command=lambda: self._handle_deactivate_swap(device_path))
|
|
|
|
# ROM 设备操作
|
|
if device_type == 'rom':
|
|
menu.add_separator()
|
|
if not mount_point or mount_point == '' or mount_point == 'N/A':
|
|
menu.add_command(label=f"挂载为 Loop {device_path}...",
|
|
command=lambda: self._handle_mount_loop(device_path))
|
|
else:
|
|
menu.add_command(label=f"卸载 Loop {device_path}",
|
|
command=lambda: self._handle_unmount_loop(device_path))
|
|
|
|
if menu.index(tk.END) is not None:
|
|
self._show_context_menu(menu, event.x_root, event.y_root)
|
|
else:
|
|
menu.destroy()
|
|
logger.info("右键点击了空白区域或设备没有可用的操作。")
|
|
|
|
# === RAID 右键菜单 ===
|
|
def _on_raid_right_click(self, event):
|
|
"""RAID 右键点击事件"""
|
|
# 先关闭任何已打开的菜单
|
|
self._close_context_menu()
|
|
|
|
item = self.tree_raid.identify_row(event.y)
|
|
if item:
|
|
self.tree_raid.selection_set(item)
|
|
|
|
menu = tk.Menu(self.root, tearoff=0)
|
|
menu.add_command(label="创建 RAID 阵列...", command=self._handle_create_raid_array)
|
|
menu.add_separator()
|
|
|
|
if item:
|
|
parent = self.tree_raid.parent(item)
|
|
if not parent: # 顶层项
|
|
array_data = self._get_item_data(self.tree_raid, item)
|
|
if array_data:
|
|
array_path = array_data.get('device')
|
|
array_state = array_data.get('state', 'N/A')
|
|
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
|
|
array_uuid = array_data.get('uuid')
|
|
|
|
if array_state == 'Stopped (Configured)':
|
|
menu.add_command(label=f"激活阵列 {array_path}",
|
|
command=lambda: self._handle_activate_raid_array(array_path))
|
|
menu.add_command(label=f"删除配置文件条目 (UUID: {array_uuid})",
|
|
command=lambda: self._handle_delete_configured_raid_array(array_uuid))
|
|
else:
|
|
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
|
|
|
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
|
|
menu.add_command(label=f"卸载 {array_path} ({current_mount_point})",
|
|
command=lambda: self._unmount_and_refresh(array_path))
|
|
else:
|
|
menu.add_command(label=f"挂载 {array_path}...",
|
|
command=lambda: self._handle_mount(array_path))
|
|
menu.add_separator()
|
|
|
|
menu.add_command(label=f"解除占用 {array_path}",
|
|
command=lambda: self._handle_resolve_device_occupation(array_path))
|
|
menu.add_separator()
|
|
|
|
menu.add_command(label=f"停止阵列 {array_path}",
|
|
command=lambda: self._handle_stop_raid_array(array_path))
|
|
menu.add_command(label=f"删除阵列 {array_path}",
|
|
command=lambda ap=array_path, md=member_devices, au=array_uuid, ad=array_data:
|
|
self._handle_delete_active_raid_array(ap, md, au))
|
|
menu.add_command(label=f"格式化阵列 {array_path}...",
|
|
command=lambda: self._handle_format_raid_array(array_path))
|
|
|
|
# 文件系统检查和修复
|
|
dev_details = self.system_manager.get_device_details_by_path(array_path)
|
|
fstype = dev_details.get('fstype', '') if dev_details else ''
|
|
if fstype:
|
|
fs_menu = tk.Menu(menu, tearoff=0)
|
|
if fstype.startswith('ext'):
|
|
fs_menu.add_command(label=f"检查文件系统 (fsck) {array_path}",
|
|
command=lambda dp=array_path, ft=fstype: self._handle_fsck(dp, ft))
|
|
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {array_path}",
|
|
command=lambda dp=array_path, ft=fstype: self._handle_resize2fs(dp, ft))
|
|
elif fstype == 'xfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {array_path}",
|
|
command=lambda dp=array_path: self._handle_xfs_repair(dp))
|
|
elif fstype.lower() == 'ntfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (ntfsfix) {array_path}",
|
|
command=lambda dp=array_path: self._handle_ntfsfix(dp))
|
|
if fs_menu.index(tk.END) is not None:
|
|
menu.add_cascade(label=f"文件系统工具 {array_path}", menu=fs_menu)
|
|
|
|
if menu.index(tk.END) is not None:
|
|
self._show_context_menu(menu, event.x_root, event.y_root)
|
|
else:
|
|
menu.destroy()
|
|
|
|
# === LVM 右键菜单 ===
|
|
def _on_lvm_right_click(self, event):
|
|
"""LVM 右键点击事件"""
|
|
# 先关闭任何已打开的菜单
|
|
self._close_context_menu()
|
|
|
|
item = self.tree_lvm.identify_row(event.y)
|
|
if item:
|
|
self.tree_lvm.selection_set(item)
|
|
|
|
menu = tk.Menu(self.root, tearoff=0)
|
|
|
|
create_menu = tk.Menu(menu, tearoff=0)
|
|
create_menu.add_command(label="创建物理卷 (PV)...", command=self._handle_create_pv)
|
|
create_menu.add_command(label="创建卷组 (VG)...", command=self._handle_create_vg)
|
|
create_menu.add_command(label="创建逻辑卷 (LV)...", command=self._handle_create_lv)
|
|
menu.add_cascade(label="创建...", menu=create_menu)
|
|
menu.add_separator()
|
|
|
|
if item:
|
|
item_data = self._get_item_data(self.tree_lvm, item)
|
|
if item_data:
|
|
item_type = item_data.get('type')
|
|
data = item_data.get('data', {})
|
|
|
|
if item_type == 'pv':
|
|
pv_name = data.get('pv_name')
|
|
if pv_name and pv_name != 'N/A':
|
|
menu.add_command(label=f"删除物理卷 {pv_name}",
|
|
command=lambda: self._handle_delete_pv(pv_name))
|
|
|
|
elif item_type == 'vg':
|
|
vg_name = data.get('vg_name')
|
|
if vg_name and vg_name != 'N/A':
|
|
menu.add_command(label=f"删除卷组 {vg_name}",
|
|
command=lambda: self._handle_delete_vg(vg_name))
|
|
|
|
elif item_type == 'lv':
|
|
lv_name = data.get('lv_name')
|
|
vg_name = data.get('vg_name')
|
|
lv_attr = data.get('lv_attr', '')
|
|
lv_path = data.get('lv_path')
|
|
|
|
if lv_name and vg_name and lv_path and lv_path != 'N/A':
|
|
if 'a' in lv_attr:
|
|
menu.add_command(label=f"停用逻辑卷 {lv_name}",
|
|
command=lambda: self._handle_deactivate_lv(lv_name, vg_name))
|
|
else:
|
|
menu.add_command(label=f"激活逻辑卷 {lv_name}",
|
|
command=lambda: self._handle_activate_lv(lv_name, vg_name))
|
|
|
|
current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path)
|
|
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
|
|
menu.add_command(label=f"卸载 {lv_name} ({current_mount_point})",
|
|
command=lambda: self._unmount_and_refresh(lv_path))
|
|
else:
|
|
menu.add_command(label=f"挂载 {lv_name}...",
|
|
command=lambda: self._handle_mount(lv_path))
|
|
menu.add_separator()
|
|
|
|
menu.add_command(label=f"解除占用 {lv_path}",
|
|
command=lambda: self._handle_resolve_device_occupation(lv_path))
|
|
menu.add_separator()
|
|
|
|
menu.add_command(label=f"删除逻辑卷 {lv_name}",
|
|
command=lambda: self._handle_delete_lv(lv_name, vg_name))
|
|
menu.add_command(label=f"格式化逻辑卷 {lv_name}...",
|
|
command=lambda lp=lv_path, ln=lv_name: self._handle_format_partition(
|
|
lp, {'name': ln, 'path': lp}))
|
|
|
|
# 文件系统检查和修复
|
|
dev_details = self.system_manager.get_device_details_by_path(lv_path)
|
|
fstype = dev_details.get('fstype', '') if dev_details else ''
|
|
if fstype:
|
|
fs_menu = tk.Menu(menu, tearoff=0)
|
|
if fstype.startswith('ext'):
|
|
fs_menu.add_command(label=f"检查文件系统 (fsck) {lv_path}",
|
|
command=lambda dp=lv_path, ft=fstype: self._handle_fsck(dp, ft))
|
|
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {lv_path}",
|
|
command=lambda dp=lv_path, ft=fstype: self._handle_resize2fs(dp, ft))
|
|
elif fstype == 'xfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {lv_path}",
|
|
command=lambda dp=lv_path: self._handle_xfs_repair(dp))
|
|
elif fstype.lower() == 'ntfs':
|
|
fs_menu.add_command(label=f"修复文件系统 (ntfsfix) {lv_path}",
|
|
command=lambda dp=lv_path: self._handle_ntfsfix(dp))
|
|
if fs_menu.index(tk.END) is not None:
|
|
menu.add_cascade(label=f"文件系统工具 {lv_path}", menu=fs_menu)
|
|
|
|
if menu.index(tk.END) is not None:
|
|
self._show_context_menu(menu, event.x_root, event.y_root)
|
|
else:
|
|
menu.destroy()
|
|
|
|
# === 块设备处理函数 ===
|
|
def _handle_resolve_device_occupation(self, device_path: str) -> bool:
|
|
"""处理设备占用问题"""
|
|
success = self.occupation_resolver.resolve_occupation(device_path)
|
|
if success:
|
|
self.refresh_all_info()
|
|
return success
|
|
|
|
def _handle_deactivate_swap(self, device_path):
|
|
"""处理关闭交换分区"""
|
|
if messagebox.askyesno("确认关闭交换分区",
|
|
f"您确定要关闭交换分区 {device_path} 吗?",
|
|
default=messagebox.NO):
|
|
logger.info(f"尝试关闭交换分区 {device_path}。")
|
|
success, _, stderr = self.lvm_ops._execute_shell_command(
|
|
["swapoff", device_path],
|
|
f"关闭交换分区 {device_path} 失败",
|
|
show_dialog=True
|
|
)
|
|
if success:
|
|
messagebox.showinfo("成功", f"交换分区 {device_path} 已成功关闭。")
|
|
self.refresh_all_info()
|
|
return True
|
|
else:
|
|
logger.error(f"关闭交换分区 {device_path} 失败: {stderr}")
|
|
else:
|
|
logger.info(f"用户取消了关闭交换分区 {device_path}。")
|
|
return False
|
|
|
|
def _handle_mount_loop(self, device_path):
|
|
"""处理挂载 ROM 设备为 loop"""
|
|
if self.disk_ops.mount_loop_device(device_path):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_unmount_loop(self, device_path):
|
|
"""处理卸载 loop 设备"""
|
|
if self.disk_ops.unmount_loop_device(device_path):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_show_smart(self, device_path):
|
|
"""显示指定设备的 SMART 信息"""
|
|
monitor = SmartMonitor()
|
|
if not monitor.is_available():
|
|
messagebox.showwarning("警告",
|
|
"smartctl 工具未安装。\n"
|
|
"请安装 smartmontools:\n"
|
|
" CentOS/RHEL: sudo yum install smartmontools\n"
|
|
" Ubuntu/Debian: sudo apt-get install smartmontools")
|
|
return
|
|
|
|
smart_info = monitor.get_disk_smart_info(device_path)
|
|
if smart_info:
|
|
SmartInfoDialog(self.root, device_path, smart_info)
|
|
else:
|
|
messagebox.showerror("错误",
|
|
f"无法获取 {device_path} 的 SMART 信息。\n"
|
|
f"可能的原因:\n"
|
|
f"1. 设备不支持 SMART\n"
|
|
f"2. 需要 root 权限\n"
|
|
f"3. 设备未正确连接")
|
|
|
|
def _show_smart_overview(self):
|
|
"""显示 SMART 总览"""
|
|
monitor = SmartMonitor()
|
|
if not monitor.is_available():
|
|
messagebox.showwarning("警告",
|
|
"smartctl 工具未安装。\n"
|
|
"请安装 smartmontools:\n"
|
|
" CentOS/RHEL: sudo yum install smartmontools\n"
|
|
" Ubuntu/Debian: sudo apt-get install smartmontools")
|
|
return
|
|
|
|
SmartOverviewDialog(self.root)
|
|
|
|
def _handle_wipe_partition_table(self, device_path):
|
|
"""处理擦除物理盘分区表"""
|
|
if messagebox.askyesno("确认擦除分区表",
|
|
f"您确定要擦除设备 {device_path} 上的所有分区表吗?\n"
|
|
f"**此操作将永久删除设备上的所有分区和数据,且不可恢复!**\n"
|
|
f"请谨慎操作!",
|
|
default=messagebox.NO):
|
|
logger.warning(f"尝试擦除 {device_path} 上的分区表。")
|
|
success, _, stderr = self.lvm_ops._execute_shell_command(
|
|
["parted", "-s", device_path, "mklabel", "gpt"],
|
|
f"擦除 {device_path} 上的分区表失败"
|
|
)
|
|
if success:
|
|
messagebox.showinfo("成功", f"设备 {device_path} 上的分区表已成功擦除。")
|
|
self.refresh_all_info()
|
|
|
|
def _handle_create_partition(self, disk_path, dev_data):
|
|
"""处理创建分区"""
|
|
total_disk_mib = 0.0
|
|
total_size_str = dev_data.get('size')
|
|
logger.debug(f"尝试为磁盘 {disk_path} 创建分区。原始大小字符串: '{total_size_str}'")
|
|
|
|
if total_size_str:
|
|
match = re.match(r'(\d+(\.\d+)?)\s*([KMGT]?B?)', total_size_str, re.IGNORECASE)
|
|
if match:
|
|
value = float(match.group(1))
|
|
unit = match.group(3).upper() if match.group(3) else ''
|
|
|
|
if unit == 'KB' or unit == 'K':
|
|
total_disk_mib = value / 1024
|
|
elif unit == 'MB' or unit == 'M':
|
|
total_disk_mib = value
|
|
elif unit == 'GB' or unit == 'G':
|
|
total_disk_mib = value * 1024
|
|
elif unit == 'TB' or unit == 'T':
|
|
total_disk_mib = value * 1024 * 1024
|
|
elif unit == 'B':
|
|
total_disk_mib = value / (1024 * 1024)
|
|
else:
|
|
logger.warning(f"无法识别磁盘 {disk_path} 的大小单位: '{unit}'")
|
|
total_disk_mib = 0.0
|
|
logger.debug(f"解析后的磁盘总大小 (MiB): {total_disk_mib}")
|
|
else:
|
|
logger.warning(f"无法解析磁盘 {disk_path} 的大小字符串 '{total_size_str}'。")
|
|
total_disk_mib = 0.0
|
|
else:
|
|
logger.warning(f"获取磁盘 {disk_path} 的大小字符串为空或None。")
|
|
total_disk_mib = 0.0
|
|
|
|
if total_disk_mib <= 0.0:
|
|
messagebox.showerror("错误", f"无法获取磁盘 {disk_path} 的有效总大小。")
|
|
return
|
|
|
|
start_position_mib = 0.0
|
|
max_available_mib = total_disk_mib
|
|
|
|
if dev_data.get('children'):
|
|
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
|
|
calculated_start_mib, largest_free_space_mib = self.disk_ops.get_disk_free_space_info_mib(disk_path, total_disk_mib)
|
|
if calculated_start_mib is None or largest_free_space_mib is None:
|
|
messagebox.showerror("错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
|
return
|
|
start_position_mib = calculated_start_mib
|
|
max_available_mib = largest_free_space_mib
|
|
if max_available_mib < 0:
|
|
max_available_mib = 0.0
|
|
else:
|
|
logger.debug(f"磁盘 {disk_path} 没有现有分区。")
|
|
max_available_mib = max(0.0, total_disk_mib - 1.0)
|
|
start_position_mib = 1.0
|
|
|
|
# 使用增强型分区创建对话框(带滑块可视化)
|
|
dialog = EnhancedPartitionDialog(self.root, disk_path, total_disk_mib, max_available_mib)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.disk_ops.create_partition(
|
|
info['disk_path'],
|
|
info['partition_table_type'],
|
|
info['size_gb'],
|
|
info['total_disk_mib'],
|
|
info['use_max_space']
|
|
):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_mount(self, device_path):
|
|
"""处理挂载"""
|
|
dialog = MountDialog(self.root, device_path)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.disk_ops.mount_partition(device_path, info['mount_point'], info['add_to_fstab']):
|
|
self.refresh_all_info()
|
|
|
|
def _unmount_and_refresh(self, device_path):
|
|
"""处理卸载并刷新"""
|
|
if self.disk_ops.unmount_partition(device_path, show_dialog_on_error=True):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_partition(self, device_path):
|
|
"""处理删除分区"""
|
|
if self.disk_ops.delete_partition(device_path):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_format_partition(self, device_path, dev_data=None):
|
|
"""处理格式化分区 - 使用增强型对话框"""
|
|
# 获取设备信息
|
|
if dev_data is None:
|
|
dev_data = self.system_manager.get_device_details_by_path(device_path)
|
|
|
|
# 使用增强型格式化对话框
|
|
dialog = EnhancedFormatDialog(self.root, device_path, dev_data or {})
|
|
fs_type = dialog.wait_for_result()
|
|
|
|
if fs_type:
|
|
# 执行格式化
|
|
self.disk_ops.format_partition(device_path, fs_type)
|
|
|
|
def _handle_fsck(self, device_path, fstype):
|
|
"""检查/修复 ext 文件系统"""
|
|
# 检查设备是否已挂载
|
|
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
|
|
if mount_point and mount_point != 'N/A':
|
|
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行文件系统检查。")
|
|
return
|
|
|
|
if not messagebox.askyesno("确认",
|
|
f"即将对 {device_path} ({fstype}) 执行文件系统检查。\n"
|
|
f"命令: fsck -f -y {device_path}\n\n"
|
|
f"注意:此操作可能需要较长时间,请勿中断。",
|
|
default=messagebox.NO):
|
|
return
|
|
|
|
logger.info(f"开始检查文件系统: {device_path} ({fstype})")
|
|
success, stdout, stderr = self.lvm_ops._execute_shell_command(
|
|
["fsck", "-f", "-y", device_path],
|
|
f"检查文件系统 {device_path} 失败"
|
|
)
|
|
|
|
if success:
|
|
messagebox.showinfo("成功", f"文件系统检查完成:\n{stdout[:500] if stdout else '无输出'}")
|
|
else:
|
|
messagebox.showerror("错误", f"文件系统检查失败:\n{stderr[:500] if stderr else '未知错误'}")
|
|
self.refresh_all_info()
|
|
|
|
def _handle_xfs_repair(self, device_path):
|
|
"""修复 XFS 文件系统"""
|
|
# 检查设备是否已挂载
|
|
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
|
|
if mount_point and mount_point != 'N/A':
|
|
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行文件系统修复。")
|
|
return
|
|
|
|
if not messagebox.askyesno("确认",
|
|
f"即将对 {device_path} (XFS) 执行文件系统修复。\n"
|
|
f"命令: xfs_repair {device_path}\n\n"
|
|
f"警告:此操作会尝试修复文件系统错误,但可能无法恢复所有数据。\n"
|
|
f"建议先备份重要数据。",
|
|
default=messagebox.NO):
|
|
return
|
|
|
|
logger.info(f"开始修复 XFS 文件系统: {device_path}")
|
|
success, stdout, stderr = self.lvm_ops._execute_shell_command(
|
|
["xfs_repair", device_path],
|
|
f"修复 XFS 文件系统 {device_path} 失败"
|
|
)
|
|
|
|
if success:
|
|
messagebox.showinfo("成功", f"XFS 文件系统修复完成:\n{stdout[:500] if stdout else '无输出'}")
|
|
else:
|
|
messagebox.showerror("错误", f"XFS 文件系统修复失败:\n{stderr[:500] if stderr else '未知错误'}")
|
|
self.refresh_all_info()
|
|
|
|
def _handle_resize2fs(self, device_path, fstype):
|
|
"""调整 ext 文件系统大小"""
|
|
# 检查设备是否已挂载
|
|
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
|
|
if mount_point and mount_point != 'N/A':
|
|
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行调整大小操作。")
|
|
return
|
|
|
|
if not messagebox.askyesno("确认",
|
|
f"即将对 {device_path} ({fstype}) 强制调整文件系统大小以匹配分区大小。\n"
|
|
f"命令: resize2fs -f {device_path}\n\n"
|
|
f"注意:此操作会强制调整文件系统大小,请确保分区大小已正确设置。",
|
|
default=messagebox.NO):
|
|
return
|
|
|
|
logger.info(f"开始调整文件系统大小: {device_path} ({fstype})")
|
|
success, stdout, stderr = self.lvm_ops._execute_shell_command(
|
|
["resize2fs", "-f", device_path],
|
|
f"调整文件系统大小 {device_path} 失败"
|
|
)
|
|
|
|
if success:
|
|
messagebox.showinfo("成功", f"文件系统大小调整完成:\n{stdout[:500] if stdout else '无输出'}")
|
|
else:
|
|
messagebox.showerror("错误", f"调整文件系统大小失败:\n{stderr[:500] if stderr else '未知错误'}")
|
|
self.refresh_all_info()
|
|
|
|
def _handle_ntfsfix(self, device_path):
|
|
"""修复 NTFS 文件系统"""
|
|
# 检查设备是否已挂载
|
|
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
|
|
if mount_point and mount_point != 'N/A':
|
|
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行文件系统修复。")
|
|
return
|
|
|
|
if not messagebox.askyesno("确认",
|
|
f"即将对 {device_path} (NTFS) 执行文件系统修复。\n"
|
|
f"命令: sudo ntfsfix -d {device_path}\n\n"
|
|
f"注意:此操作会清除 NTFS 卷的脏标志并尝试修复。\n"
|
|
f"如果修复后仍无法挂载,请尝试使用 ntfs-3g 方式挂载。",
|
|
default=messagebox.NO):
|
|
return
|
|
|
|
logger.info(f"开始修复 NTFS 文件系统: {device_path}")
|
|
success, stdout, stderr = self.lvm_ops._execute_shell_command(
|
|
["ntfsfix", "-d", device_path],
|
|
f"修复 NTFS 文件系统 {device_path} 失败"
|
|
)
|
|
|
|
if success:
|
|
messagebox.showinfo("成功", f"NTFS 文件系统修复完成:\n{stdout[:500] if stdout else '无输出'}\n\n"
|
|
f"如果仍无法正常挂载,请尝试使用:\n"
|
|
f"mount -t ntfs-3g {device_path} <挂载点>")
|
|
else:
|
|
messagebox.showerror("错误", f"NTFS 文件系统修复失败:\n{stderr[:500] if stderr else '未知错误'}\n\n"
|
|
f"建议尝试使用 ntfs-3g 驱动挂载。")
|
|
self.refresh_all_info()
|
|
|
|
def on_disk_formatting_finished(self, success, device_path, stdout, stderr):
|
|
"""接收格式化完成回调并刷新界面"""
|
|
logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}")
|
|
self.refresh_all_info()
|
|
|
|
# === RAID 处理函数 ===
|
|
def refresh_raid_info(self):
|
|
"""刷新 RAID 信息"""
|
|
for item in self.tree_raid.get_children():
|
|
self.tree_raid.delete(item)
|
|
|
|
try:
|
|
raid_arrays = self.system_manager.get_mdadm_arrays()
|
|
if not raid_arrays:
|
|
self.tree_raid.insert('', tk.END, text='未找到RAID阵列。', values=[''] * len(self.raid_columns))
|
|
logger.info("未找到RAID阵列。")
|
|
return
|
|
|
|
for array in raid_arrays:
|
|
array_path = array.get('device', 'N/A')
|
|
if not array_path or array_path == 'N/A':
|
|
continue
|
|
|
|
current_mount_point = ""
|
|
if array.get('state') != 'Stopped (Configured)':
|
|
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
|
|
|
values = [
|
|
array_path,
|
|
array.get('level', 'N/A'),
|
|
array.get('state', 'N/A'),
|
|
array.get('array_size', 'N/A'),
|
|
array.get('active_devices', 'N/A'),
|
|
array.get('failed_devices', 'N/A'),
|
|
array.get('spare_devices', 'N/A'),
|
|
array.get('total_devices', 'N/A'),
|
|
array.get('uuid', 'N/A'),
|
|
array.get('name', 'N/A'),
|
|
array.get('chunk_size', 'N/A'),
|
|
current_mount_point if current_mount_point else ""
|
|
]
|
|
|
|
item_id = self.tree_raid.insert('', tk.END, text=array_path, values=values)
|
|
self.tree_raid.item(item_id, tags=(str(array),))
|
|
|
|
if array.get('state') != 'Stopped (Configured)':
|
|
for member in array.get('member_devices', []):
|
|
member_values = [
|
|
f" {member.get('device_path', 'N/A')}",
|
|
f"成员: {member.get('raid_device', 'N/A')}",
|
|
member.get('state', 'N/A')
|
|
] + [''] * (len(self.raid_columns) - 3)
|
|
self.tree_raid.insert(item_id, tk.END, text='', values=member_values)
|
|
|
|
self.tree_raid.item(item_id, open=True)
|
|
|
|
logger.info("RAID阵列信息刷新成功。")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"刷新RAID阵列信息失败: {e}")
|
|
logger.error(f"刷新RAID阵列信息失败: {e}")
|
|
|
|
def _handle_activate_raid_array(self, array_path):
|
|
"""处理激活 RAID 阵列"""
|
|
selected = self.tree_raid.selection()
|
|
if not selected:
|
|
return
|
|
|
|
array_data = self._get_item_data(self.tree_raid, selected[0])
|
|
if not array_data:
|
|
return
|
|
|
|
array_uuid = array_data.get('uuid')
|
|
if not array_uuid or array_uuid == 'N/A':
|
|
messagebox.showerror("错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
|
|
return
|
|
|
|
if messagebox.askyesno("确认激活 RAID 阵列",
|
|
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
|
|
"这将尝试重新组装阵列。",
|
|
default=messagebox.NO):
|
|
if self.raid_ops.activate_raid_array(array_path, array_uuid):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_create_raid_array(self):
|
|
"""处理创建 RAID 阵列"""
|
|
available_devices = self.system_manager.get_unallocated_partitions()
|
|
|
|
if not available_devices:
|
|
messagebox.showwarning("警告", "没有可用于创建 RAID 阵列的设备。请确保有未挂载、未被LVM或RAID使用的磁盘或分区。")
|
|
return
|
|
|
|
dialog = CreateRaidDialog(self.root, available_devices)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.raid_ops.create_raid_array(info['devices'], info['level'], info['chunk_size']):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_stop_raid_array(self, array_path):
|
|
"""处理停止 RAID 阵列"""
|
|
if self.raid_ops.stop_raid_array(array_path):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid, array_data=None):
|
|
"""处理删除活动的 RAID 阵列 - 使用增强型对话框"""
|
|
# 获取阵列详情(如果未提供)
|
|
if array_data is None:
|
|
array_data = {}
|
|
try:
|
|
raid_arrays = self.system_manager.get_mdadm_arrays()
|
|
for array in raid_arrays:
|
|
if array.get('device') == array_path:
|
|
array_data = array
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# 使用增强型删除对话框
|
|
dialog = EnhancedRaidDeleteDialog(
|
|
self.root, array_path, array_data, member_devices
|
|
)
|
|
|
|
if dialog.wait_for_result():
|
|
# 执行删除
|
|
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_configured_raid_array(self, uuid):
|
|
"""处理删除停止状态 RAID 阵列的配置文件条目"""
|
|
if self.raid_ops.delete_configured_raid_array(uuid):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_format_raid_array(self, array_path):
|
|
"""处理格式化 RAID 阵列 - 使用增强型对话框"""
|
|
# 获取设备信息
|
|
dev_data = self.system_manager.get_device_details_by_path(array_path)
|
|
|
|
# 使用增强型格式化对话框
|
|
dialog = EnhancedFormatDialog(self.root, array_path, dev_data or {})
|
|
fs_type = dialog.wait_for_result()
|
|
|
|
if fs_type:
|
|
# 执行格式化
|
|
self.disk_ops.format_partition(array_path, fs_type)
|
|
|
|
# === LVM 处理函数 ===
|
|
def refresh_lvm_info(self):
|
|
"""刷新 LVM 信息"""
|
|
for item in self.tree_lvm.get_children():
|
|
self.tree_lvm.delete(item)
|
|
|
|
try:
|
|
lvm_data = self.system_manager.get_lvm_info()
|
|
|
|
if not lvm_data.get('pvs') and not lvm_data.get('vgs') and not lvm_data.get('lvs'):
|
|
self.tree_lvm.insert('', tk.END, text='未找到LVM信息。', values=[''] * len(self.lvm_columns))
|
|
logger.info("未找到LVM信息。")
|
|
return
|
|
|
|
# 物理卷 (PVs)
|
|
pv_root = self.tree_lvm.insert('', tk.END, text="物理卷 (PVs)", values=[''] * len(self.lvm_columns))
|
|
self.tree_lvm.item(pv_root, tags=(str({'type': 'pv_root'}),))
|
|
|
|
if lvm_data.get('pvs'):
|
|
for pv in lvm_data['pvs']:
|
|
pv_name = pv.get('pv_name', 'N/A')
|
|
pv_path = pv_name if pv_name.startswith('/dev/') else f"/dev/{pv_name}"
|
|
|
|
values = [
|
|
pv_name,
|
|
pv.get('pv_size', 'N/A'),
|
|
pv.get('pv_attr', 'N/A'),
|
|
pv.get('pv_uuid', 'N/A'),
|
|
f"VG: {pv.get('vg_name', 'N/A')}",
|
|
f"空闲: {pv.get('pv_free', 'N/A')}",
|
|
pv.get('pv_fmt', 'N/A'),
|
|
""
|
|
]
|
|
|
|
item_id = self.tree_lvm.insert(pv_root, tk.END, text=pv_name, values=values)
|
|
pv_data = pv.copy()
|
|
pv_data['pv_name'] = pv_path
|
|
self.tree_lvm.item(item_id, tags=(str({'type': 'pv', 'data': pv_data}),))
|
|
else:
|
|
self.tree_lvm.insert(pv_root, tk.END, text='未找到物理卷。')
|
|
|
|
# 卷组 (VGs)
|
|
vg_root = self.tree_lvm.insert('', tk.END, text="卷组 (VGs)", values=[''] * len(self.lvm_columns))
|
|
self.tree_lvm.item(vg_root, tags=(str({'type': 'vg_root'}),))
|
|
|
|
if lvm_data.get('vgs'):
|
|
for vg in lvm_data['vgs']:
|
|
vg_name = vg.get('vg_name', 'N/A')
|
|
|
|
values = [
|
|
vg_name,
|
|
vg.get('vg_size', 'N/A'),
|
|
vg.get('vg_attr', 'N/A'),
|
|
vg.get('vg_uuid', 'N/A'),
|
|
f"PVs: {vg.get('pv_count', 'N/A')}, LVs: {vg.get('lv_count', 'N/A')}",
|
|
f"空闲: {vg.get('vg_free', 'N/A')}, 已分配: {vg.get('vg_alloc_percent', 'N/A')}%",
|
|
vg.get('vg_fmt', 'N/A'),
|
|
""
|
|
]
|
|
|
|
item_id = self.tree_lvm.insert(vg_root, tk.END, text=vg_name, values=values)
|
|
vg_data = vg.copy()
|
|
vg_data['vg_name'] = vg_name
|
|
self.tree_lvm.item(item_id, tags=(str({'type': 'vg', 'data': vg_data}),))
|
|
else:
|
|
self.tree_lvm.insert(vg_root, tk.END, text='未找到卷组。')
|
|
|
|
# 逻辑卷 (LVs)
|
|
lv_root = self.tree_lvm.insert('', tk.END, text="逻辑卷 (LVs)", values=[''] * len(self.lvm_columns))
|
|
self.tree_lvm.item(lv_root, tags=(str({'type': 'lv_root'}),))
|
|
|
|
if lvm_data.get('lvs'):
|
|
for lv in lvm_data['lvs']:
|
|
lv_name = lv.get('lv_name', 'N/A')
|
|
vg_name = lv.get('vg_name', 'N/A')
|
|
lv_attr = lv.get('lv_attr', '')
|
|
|
|
lv_path = lv.get('lv_path')
|
|
if not lv_path or lv_path == 'N/A':
|
|
lv_path = f"/dev/{vg_name}/{lv_name}" if vg_name != 'N/A' and lv_name != 'N/A' else 'N/A'
|
|
|
|
current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path)
|
|
|
|
values = [
|
|
lv_name,
|
|
lv.get('lv_size', 'N/A'),
|
|
lv_attr,
|
|
lv.get('lv_uuid', 'N/A'),
|
|
f"VG: {vg_name}, Origin: {lv.get('origin', 'N/A')}",
|
|
f"快照: {lv.get('snap_percent', 'N/A')}%",
|
|
lv_path,
|
|
current_mount_point if current_mount_point else ""
|
|
]
|
|
|
|
item_id = self.tree_lvm.insert(lv_root, tk.END, text=lv_name, values=values)
|
|
lv_data = lv.copy()
|
|
lv_data['lv_path'] = lv_path
|
|
lv_data['lv_name'] = lv_name
|
|
lv_data['vg_name'] = vg_name
|
|
lv_data['lv_attr'] = lv_attr
|
|
self.tree_lvm.item(item_id, tags=(str({'type': 'lv', 'data': lv_data}),))
|
|
else:
|
|
self.tree_lvm.insert(lv_root, tk.END, text='未找到逻辑卷。')
|
|
|
|
# 展开所有根节点
|
|
self.tree_lvm.item(pv_root, open=True)
|
|
self.tree_lvm.item(vg_root, open=True)
|
|
self.tree_lvm.item(lv_root, open=True)
|
|
|
|
logger.info("LVM信息刷新成功。")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"刷新LVM信息失败: {e}")
|
|
logger.error(f"刷新LVM信息失败: {e}")
|
|
|
|
def _handle_create_pv(self):
|
|
"""处理创建物理卷"""
|
|
available_partitions = self.system_manager.get_unallocated_partitions()
|
|
if not available_partitions:
|
|
messagebox.showwarning("警告", "没有可用于创建物理卷的未分配分区。")
|
|
return
|
|
|
|
dialog = CreatePvDialog(self.root, available_partitions)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.lvm_ops.create_pv(info['device_path']):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_pv(self, device_path):
|
|
"""处理删除物理卷"""
|
|
if self.lvm_ops.delete_pv(device_path):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_create_vg(self):
|
|
"""处理创建卷组"""
|
|
lvm_info = self.system_manager.get_lvm_info()
|
|
available_pvs = []
|
|
for pv in lvm_info.get('pvs', []):
|
|
pv_name = pv.get('pv_name')
|
|
if pv_name and pv_name != 'N/A' and not pv.get('vg_name'):
|
|
if pv_name.startswith('/dev/'):
|
|
available_pvs.append(pv_name)
|
|
else:
|
|
available_pvs.append(f"/dev/{pv_name}")
|
|
|
|
if not available_pvs:
|
|
messagebox.showwarning("警告", "没有可用于创建卷组的物理卷。")
|
|
return
|
|
|
|
dialog = CreateVgDialog(self.root, available_pvs)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.lvm_ops.create_vg(info['vg_name'], info['pvs']):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_create_lv(self):
|
|
"""处理创建逻辑卷"""
|
|
lvm_info = self.system_manager.get_lvm_info()
|
|
available_vgs = []
|
|
vg_sizes = {}
|
|
|
|
for vg in lvm_info.get('vgs', []):
|
|
vg_name = vg.get('vg_name')
|
|
if vg_name and vg_name != 'N/A':
|
|
available_vgs.append(vg_name)
|
|
|
|
free_size_str = vg.get('vg_free', '0B').strip().lower()
|
|
current_vg_size_gb = 0.0
|
|
|
|
match = re.match(r'<?(\d+\.?\d*)\s*([gmktb])?', free_size_str)
|
|
if match:
|
|
value = float(match.group(1))
|
|
unit = match.group(2)
|
|
|
|
if unit == 'k':
|
|
current_vg_size_gb = value / (1024 * 1024)
|
|
elif unit == 'm':
|
|
current_vg_size_gb = value / 1024
|
|
elif unit == 'g' or unit is None:
|
|
current_vg_size_gb = value
|
|
elif unit == 't':
|
|
current_vg_size_gb = value * 1024
|
|
elif unit == 'b':
|
|
current_vg_size_gb = value / (1024 * 1024 * 1024)
|
|
|
|
vg_sizes[vg_name] = current_vg_size_gb
|
|
|
|
if not available_vgs:
|
|
messagebox.showwarning("警告", "没有可用于创建逻辑卷的卷组。")
|
|
return
|
|
|
|
dialog = CreateLvDialog(self.root, available_vgs, vg_sizes)
|
|
info = dialog.wait_for_result()
|
|
|
|
if info:
|
|
if self.lvm_ops.create_lv(info['lv_name'], info['vg_name'], info['size_gb'], info['use_max_space']):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_lv(self, lv_name, vg_name):
|
|
"""处理删除逻辑卷"""
|
|
lv_path = f"/dev/{vg_name}/{lv_name}"
|
|
self.disk_ops.unmount_partition(lv_path, show_dialog_on_error=False)
|
|
self.disk_ops._remove_fstab_entry(lv_path)
|
|
|
|
if self.lvm_ops.delete_lv(lv_name, vg_name):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_activate_lv(self, lv_name, vg_name):
|
|
"""处理激活逻辑卷"""
|
|
if self.lvm_ops.activate_lv(lv_name, vg_name):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_deactivate_lv(self, lv_name, vg_name):
|
|
"""处理停用逻辑卷"""
|
|
if self.lvm_ops.deactivate_lv(lv_name, vg_name):
|
|
self.refresh_all_info()
|
|
|
|
def _handle_delete_vg(self, vg_name):
|
|
"""处理删除卷组"""
|
|
logger.info(f"尝试删除卷组 (VG) {vg_name}。")
|
|
if not messagebox.askyesno("确认删除卷组",
|
|
f"您确定要删除卷组 {vg_name} 吗?此操作将永久删除该卷组及其所有逻辑卷和数据!",
|
|
default=messagebox.NO):
|
|
logger.info(f"用户取消了删除卷组 {vg_name} 的操作。")
|
|
return
|
|
|
|
lvm_info = self.system_manager.get_lvm_info()
|
|
vg_info = next((vg for vg in lvm_info.get('vgs', []) if vg.get('vg_name') == vg_name), None)
|
|
|
|
lv_count_raw = vg_info.get('lv_count', 0) if vg_info else 0
|
|
try:
|
|
lv_count = int(float(lv_count_raw))
|
|
except (ValueError, TypeError):
|
|
lv_count = 0
|
|
|
|
if lv_count > 0:
|
|
messagebox.showerror("删除失败", f"卷组 {vg_name} 中仍包含逻辑卷。请先删除所有逻辑卷。")
|
|
logger.error(f"尝试删除包含逻辑卷的卷组 {vg_name}。操作被阻止。")
|
|
return
|
|
|
|
success = self.lvm_ops.delete_vg(vg_name)
|
|
if success:
|
|
self.refresh_all_info()
|
|
else:
|
|
messagebox.showerror("删除卷组失败", f"删除卷组 {vg_name} 失败,请检查日志。")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 创建主窗口
|
|
root = tk.Tk()
|
|
app = MainWindow(root)
|
|
root.mainloop()
|