1054 lines
47 KiB
Python
1054 lines
47 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)
|
|
|
|
|
|
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):
|
|
"""创建界面元素"""
|
|
# 刷新按钮
|
|
self.refresh_btn = ttk.Button(self.main_frame, text="刷新数据", command=self.refresh_all_info)
|
|
self.refresh_btn.pack(fill=tk.X, pady=(0, 5))
|
|
|
|
# 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_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()
|
|
|
|
# 分区操作
|
|
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: self._handle_format_partition(device_path))
|
|
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: self._handle_delete_active_raid_array(array_path, member_devices, array_uuid))
|
|
menu.add_command(label=f"格式化阵列 {array_path}...",
|
|
command=lambda: self._handle_format_raid_array(array_path))
|
|
|
|
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: self._handle_format_partition(lv_path))
|
|
|
|
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_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 = CreatePartitionDialog(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):
|
|
"""处理格式化分区"""
|
|
self.disk_ops.format_partition(device_path)
|
|
|
|
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):
|
|
"""处理删除活动的 RAID 阵列"""
|
|
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 阵列"""
|
|
self.disk_ops.format_partition(array_path)
|
|
|
|
# === 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} 失败,请检查日志。")
|