# 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 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_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 _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("", 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("", 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("", 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('', 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('', 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() # 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)) 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)) 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)) 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 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 阵列""" 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' 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()