487 lines
21 KiB
Python
487 lines
21 KiB
Python
# dialogs.py
|
||
import os
|
||
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||
QLineEdit, QComboBox, QDoubleSpinBox,
|
||
QPushButton, QFormLayout, QFileDialog,
|
||
QMessageBox, QCheckBox, QListWidget, QListWidgetItem)
|
||
from PySide6.QtCore import Qt
|
||
|
||
from logger_config import setup_logging, logger
|
||
|
||
class CreatePartitionDialog(QDialog):
|
||
def __init__(self, parent=None, disk_path="", total_disk_mib=0.0, max_available_mib=0.0):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("创建分区")
|
||
self.setMinimumWidth(300)
|
||
|
||
self.disk_path = disk_path
|
||
self.total_disk_mib = total_disk_mib # 磁盘总大小 (MiB)
|
||
self.max_available_mib = max_available_mib # 可用空间 (MiB)
|
||
logger.debug(f"CreatePartitionDialog initialized for {disk_path}. Total MiB: {total_disk_mib}, Max Available MiB: {max_available_mib}")
|
||
|
||
self.partition_table_type_combo = QComboBox()
|
||
self.partition_table_type_combo.addItems(["gpt", "msdos"])
|
||
|
||
self.size_spinbox = QDoubleSpinBox()
|
||
# 调整最小值为 0.01 GB (约 10MB),并确保最大值不小于最小值
|
||
calculated_max_gb = self.max_available_mib / 1024.0
|
||
self.size_spinbox.setMinimum(0.01)
|
||
self.size_spinbox.setMaximum(max(0.01, calculated_max_gb)) # 将 MiB 转换为 GB 显示
|
||
logger.debug(f"Size spinbox max set to: {self.size_spinbox.maximum()} GB (calculated from {calculated_max_gb} GB)")
|
||
self.size_spinbox.setSuffix(" GB")
|
||
self.size_spinbox.setDecimals(2)
|
||
|
||
self.use_max_space_checkbox = QCheckBox("使用最大可用空间")
|
||
self.use_max_space_checkbox.setChecked(True) # 默认选中
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
self._initialize_state()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("磁盘路径:", QLabel(self.disk_path))
|
||
form_layout.addRow("分区表类型:", self.partition_table_type_combo)
|
||
form_layout.addRow("分区大小:", self.size_spinbox)
|
||
form_layout.addRow("", self.use_max_space_checkbox) # 添加复选框
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
# 信号连接保持不变,但 _toggle_size_input 方法内部将直接查询复选框状态
|
||
self.use_max_space_checkbox.stateChanged.connect(self._toggle_size_input)
|
||
|
||
def _initialize_state(self):
|
||
# 根据默认选中状态设置 spinbox
|
||
self._toggle_size_input(self.use_max_space_checkbox.checkState())
|
||
|
||
def _toggle_size_input(self, state): # state 参数仍然接收,但不再直接用于判断
|
||
# 直接查询复选框的当前状态,而不是依赖信号传递的 state 参数
|
||
is_checked = self.use_max_space_checkbox.isChecked()
|
||
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}, signal state: {state}")
|
||
|
||
if is_checked:
|
||
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
|
||
self.size_spinbox.setEnabled(False) # 禁用 spinbox
|
||
max_val = self.size_spinbox.maximum()
|
||
self.size_spinbox.setValue(max_val) # 设置为最大可用 GB
|
||
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {max_val} GB.")
|
||
else: # is_checked is False
|
||
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
|
||
self.size_spinbox.setEnabled(True) # 启用 spinbox
|
||
# 如果之前是最大值,取消勾选后,恢复到最小值,方便用户输入自定义值
|
||
if self.size_spinbox.value() == self.size_spinbox.maximum():
|
||
min_val = self.size_spinbox.minimum()
|
||
self.size_spinbox.setValue(min_val)
|
||
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: {min_val} GB.")
|
||
else:
|
||
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {self.size_spinbox.value()} GB.")
|
||
|
||
def get_partition_info(self):
|
||
size_gb = self.size_spinbox.value()
|
||
use_max_space = self.use_max_space_checkbox.isChecked()
|
||
|
||
if not use_max_space and size_gb <= 0:
|
||
QMessageBox.warning(self, "输入错误", "分区大小必须大于0。")
|
||
return None
|
||
# 这里的检查应该使用 self.size_spinbox.maximum() 来判断,因为它是实际的最大值
|
||
if not use_max_space and size_gb > self.size_spinbox.maximum():
|
||
QMessageBox.warning(self, "输入错误", "分区大小不能超过最大可用空间。")
|
||
return None
|
||
|
||
return {
|
||
'disk_path': self.disk_path,
|
||
'partition_table_type': self.partition_table_type_combo.currentText(),
|
||
'size_gb': size_gb,
|
||
'total_disk_mib': self.total_disk_mib, # 传递磁盘总大小 (MiB)
|
||
'use_max_space': use_max_space # 返回此标志
|
||
}
|
||
|
||
class MountDialog(QDialog):
|
||
def __init__(self, parent=None, device_path=""):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("挂载分区")
|
||
self.setMinimumWidth(300)
|
||
|
||
self.device_path = device_path
|
||
self.mount_point_input = QLineEdit()
|
||
self.add_to_fstab_checkbox = QCheckBox("开机自动挂载 (添加到 /etc/fstab)") # 新增
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("设备路径:", QLabel(self.device_path))
|
||
form_layout.addRow("挂载点:", self.mount_point_input)
|
||
form_layout.addRow("", self.add_to_fstab_checkbox) # 添加复选框
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
# 默认挂载点,可以根据设备名生成
|
||
default_mount_point = f"/mnt/{os.path.basename(self.device_path)}"
|
||
self.mount_point_input.setText(default_mount_point)
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
|
||
def get_mount_info(self):
|
||
mount_point = self.mount_point_input.text().strip()
|
||
add_to_fstab = self.add_to_fstab_checkbox.isChecked()
|
||
|
||
if not mount_point:
|
||
QMessageBox.warning(self, "输入错误", "挂载点不能为空。")
|
||
return None
|
||
if not mount_point.startswith('/'):
|
||
QMessageBox.warning(self, "输入错误", "挂载点必须是绝对路径 (以 '/' 开头)。")
|
||
return None
|
||
|
||
return {
|
||
'mount_point': mount_point,
|
||
'add_to_fstab': add_to_fstab
|
||
}
|
||
|
||
class CreateRaidDialog(QDialog):
|
||
def __init__(self, parent=None, available_devices=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("创建 RAID 阵列")
|
||
self.setMinimumWidth(350)
|
||
|
||
self.available_devices = available_devices if available_devices is not None else []
|
||
self.selected_devices = []
|
||
|
||
self.raid_level_combo = QComboBox()
|
||
self.raid_level_combo.addItems(["raid0", "raid1", "raid5", "raid6", "raid10"])
|
||
|
||
self.chunk_size_spinbox = QDoubleSpinBox()
|
||
self.chunk_size_spinbox.setMinimum(4)
|
||
self.chunk_size_spinbox.setMaximum(1024)
|
||
self.chunk_size_spinbox.setSingleStep(4)
|
||
self.chunk_size_spinbox.setValue(512) # 默认值
|
||
self.chunk_size_spinbox.setSuffix(" KB")
|
||
|
||
self.device_list_widget = QListWidget()
|
||
for dev in self.available_devices:
|
||
item = QListWidgetItem(dev)
|
||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||
item.setCheckState(Qt.Unchecked)
|
||
self.device_list_widget.addItem(item)
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("RAID 级别:", self.raid_level_combo)
|
||
form_layout.addRow("Chunk 大小:", self.chunk_size_spinbox)
|
||
form_layout.addRow("选择设备:", self.device_list_widget)
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
|
||
def get_raid_info(self):
|
||
self.selected_devices = []
|
||
for i in range(self.device_list_widget.count()):
|
||
item = self.device_list_widget.item(i)
|
||
if item.checkState() == Qt.Checked:
|
||
self.selected_devices.append(item.text())
|
||
|
||
if not self.selected_devices:
|
||
QMessageBox.warning(self, "输入错误", "请选择至少一个设备来创建 RAID 阵列。")
|
||
return None
|
||
|
||
raid_level = self.raid_level_combo.currentText()
|
||
num_devices = len(self.selected_devices)
|
||
|
||
# RAID level specific checks for minimum devices
|
||
if raid_level == "raid0" and num_devices < 1: # RAID0 can be 1 device in some contexts, but usually >1
|
||
QMessageBox.warning(self, "输入错误", "RAID0 至少需要一个设备。")
|
||
return None
|
||
elif raid_level == "raid1" and num_devices < 2:
|
||
QMessageBox.warning(self, "输入错误", "RAID1 至少需要两个设备。")
|
||
return None
|
||
elif raid_level == "raid5" and num_devices < 3:
|
||
QMessageBox.warning(self, "输入错误", "RAID5 至少需要三个设备。")
|
||
return None
|
||
elif raid_level == "raid6" and num_devices < 4:
|
||
QMessageBox.warning(self, "输入错误", "RAID6 至少需要四个设备。")
|
||
return None
|
||
elif raid_level == "raid10" and num_devices < 2: # RAID10 needs at least 2 (for 1+0)
|
||
QMessageBox.warning(self, "输入错误", "RAID10 至少需要两个设备。")
|
||
return None
|
||
|
||
return {
|
||
'devices': self.selected_devices,
|
||
'level': self.raid_level_combo.currentText().replace('raid', ''), # 移除 'raid' 前缀
|
||
'chunk_size': int(self.chunk_size_spinbox.value()) # KB
|
||
}
|
||
|
||
class CreatePvDialog(QDialog):
|
||
def __init__(self, parent=None, available_partitions=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("创建物理卷 (PV)")
|
||
self.setMinimumWidth(300)
|
||
|
||
self.available_partitions = available_partitions if available_partitions is not None else []
|
||
|
||
self.device_combo_box = QComboBox()
|
||
self.device_combo_box.addItems(self.available_partitions)
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("选择设备:", self.device_combo_box)
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
|
||
def get_pv_info(self):
|
||
device_path = self.device_combo_box.currentText()
|
||
if not device_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择一个设备来创建物理卷。")
|
||
return None
|
||
return {'device_path': device_path}
|
||
|
||
class CreateVgDialog(QDialog):
|
||
def __init__(self, parent=None, available_pvs=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("创建卷组 (VG)")
|
||
self.setMinimumWidth(300)
|
||
|
||
self.available_pvs = available_pvs if available_pvs is not None else []
|
||
self.selected_pvs = []
|
||
|
||
self.vg_name_input = QLineEdit()
|
||
self.pv_list_widget = QListWidget()
|
||
for pv in self.available_pvs:
|
||
item = QListWidgetItem(pv)
|
||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||
item.setCheckState(Qt.Unchecked)
|
||
self.pv_list_widget.addItem(item)
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("卷组名称:", self.vg_name_input)
|
||
form_layout.addRow("选择物理卷:", self.pv_list_widget)
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
|
||
def get_vg_info(self):
|
||
vg_name = self.vg_name_input.text().strip()
|
||
self.selected_pvs = []
|
||
for i in range(self.pv_list_widget.count()):
|
||
item = self.pv_list_widget.item(i)
|
||
if item.checkState() == Qt.Checked:
|
||
self.selected_pvs.append(item.text())
|
||
|
||
if not vg_name:
|
||
QMessageBox.warning(self, "输入错误", "卷组名称不能为空。")
|
||
return None
|
||
if not self.selected_pvs:
|
||
QMessageBox.warning(self, "输入错误", "请选择至少一个物理卷来创建卷组。")
|
||
return None
|
||
|
||
return {
|
||
'vg_name': vg_name,
|
||
'pvs': self.selected_pvs
|
||
}
|
||
|
||
class CreateLvDialog(QDialog):
|
||
def __init__(self, parent=None, available_vgs=None, vg_sizes=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("创建逻辑卷")
|
||
self.setMinimumWidth(300)
|
||
|
||
self.available_vgs = available_vgs if available_vgs is not None else []
|
||
self.vg_sizes = vg_sizes if vg_sizes is not None else {} # 存储每个 VG 的可用大小 (GB)
|
||
logger.debug(f"CreateLvDialog initialized. Available VGs: {self.available_vgs}, VG Sizes: {self.vg_sizes}")
|
||
|
||
self.lv_name_input = QLineEdit()
|
||
self.vg_combo_box = QComboBox()
|
||
self.lv_size_spinbox = QDoubleSpinBox()
|
||
self.use_max_space_checkbox = QCheckBox("使用最大可用空间")
|
||
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
self._initialize_state()
|
||
|
||
def _setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
form_layout = QFormLayout()
|
||
form_layout.addRow("逻辑卷名称:", self.lv_name_input)
|
||
form_layout.addRow("选择卷组:", self.vg_combo_box)
|
||
form_layout.addRow("逻辑卷大小 (GB):", self.lv_size_spinbox)
|
||
form_layout.addRow("", self.use_max_space_checkbox)
|
||
|
||
layout.addLayout(form_layout)
|
||
|
||
button_box = QHBoxLayout()
|
||
self.confirm_button = QPushButton("确定")
|
||
self.cancel_button = QPushButton("取消")
|
||
button_box.addWidget(self.confirm_button)
|
||
button_box.addWidget(self.cancel_button)
|
||
layout.addLayout(button_box)
|
||
|
||
self.lv_size_spinbox.setMinimum(0.01) # 逻辑卷最小大小,例如 0.01 GB
|
||
self.lv_size_spinbox.setSuffix(" GB")
|
||
self.lv_size_spinbox.setDecimals(2)
|
||
# 设置一个较大的初始最大值,它将在 _update_size_options 中被实际的 VG 大小覆盖
|
||
self.lv_size_spinbox.setMaximum(100000.0) # 初始设置一个足够大的最大值
|
||
|
||
def _connect_signals(self):
|
||
self.confirm_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
self.vg_combo_box.currentIndexChanged.connect(self._update_size_options)
|
||
# 信号连接到不带参数的槽函数
|
||
self.use_max_space_checkbox.stateChanged.connect(self._toggle_size_input)
|
||
|
||
def _initialize_state(self):
|
||
self.vg_combo_box.addItems(self.available_vgs)
|
||
# 默认选中“使用最大可用空间”,这将触发 _toggle_size_input
|
||
self.use_max_space_checkbox.setChecked(True)
|
||
# 首次调用以设置初始状态和根据复选框状态设置值
|
||
# 这一步是必要的,即使 setChecked(True) 也会触发一次 _toggle_size_input,
|
||
# 但 _update_size_options 会在设置了正确的最大值后再次调用 _toggle_size_input,确保最终状态正确。
|
||
self._update_size_options()
|
||
logger.debug("CreateLvDialog _initialize_state completed.")
|
||
|
||
|
||
def _update_size_options(self):
|
||
"""
|
||
根据选中的卷组更新逻辑卷大小的范围 (最小值和最大值)。
|
||
并根据当前“使用最大可用空间”复选框的状态调整 spinbox 的值和启用状态。
|
||
"""
|
||
selected_vg = self.vg_combo_box.currentText()
|
||
max_size_gb = self.vg_sizes.get(selected_vg, 0.0)
|
||
logger.debug(f"[_update_size_options] Selected VG: {selected_vg}, Max available GB: {max_size_gb}")
|
||
|
||
# 确保实际最大可用空间至少大于或等于 spinbox 的最小值
|
||
# 避免当可用空间非常小时,spinbox 无法设置有效值
|
||
effective_max_size_gb = max(self.lv_size_spinbox.minimum(), max_size_gb)
|
||
self.lv_size_spinbox.setMaximum(effective_max_size_gb)
|
||
logger.debug(f"[_update_size_options] Spinbox max set to: {effective_max_size_gb} GB.")
|
||
|
||
# 在设置了新的最大值后,重新应用复选框的状态来更新值和启用状态
|
||
self._toggle_size_input() # 调用不带参数的槽函数
|
||
logger.debug("[_update_size_options] Completed.")
|
||
|
||
def _toggle_size_input(self): # 不再接收 'state' 参数
|
||
"""
|
||
根据“使用最大可用空间”复选框的状态切换大小输入框的启用/禁用状态和值。
|
||
"""
|
||
is_checked = self.use_max_space_checkbox.isChecked() # 直接查询复选框的当前状态
|
||
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
|
||
|
||
current_max_spinbox_value = self.lv_size_spinbox.maximum()
|
||
current_spinbox_value = self.lv_size_spinbox.value()
|
||
|
||
if is_checked:
|
||
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
|
||
self.lv_size_spinbox.setDisabled(True) # 禁用 spinbox
|
||
self.lv_size_spinbox.setValue(current_max_spinbox_value) # 设置为最大可用 GB
|
||
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {current_max_spinbox_value} GB.")
|
||
else: # is_checked is False
|
||
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
|
||
self.lv_size_spinbox.setDisabled(False) # 启用 spinbox
|
||
# 如果之前是最大值(因为复选框被勾选),取消勾选后,恢复到最小值,方便用户输入自定义值
|
||
if current_spinbox_value == current_max_spinbox_value:
|
||
min_val = self.lv_size_spinbox.minimum()
|
||
self.lv_size_spinbox.setValue(min_val)
|
||
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: {min_val} GB.")
|
||
else:
|
||
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {self.lv_size_spinbox.value()} GB.")
|
||
logger.debug("[_toggle_size_input] Completed.")
|
||
|
||
def get_lv_info(self):
|
||
lv_name = self.lv_name_input.text().strip()
|
||
vg_name = self.vg_combo_box.currentText()
|
||
size_gb = self.lv_size_spinbox.value()
|
||
use_max_space = self.use_max_space_checkbox.isChecked() # 获取复选框状态
|
||
|
||
if not lv_name:
|
||
QMessageBox.warning(self, "输入错误", "逻辑卷名称不能为空。")
|
||
return None
|
||
if not vg_name:
|
||
QMessageBox.warning(self, "输入错误", "请选择一个卷组。")
|
||
return None
|
||
# 如果没有使用最大空间,则检查输入大小的有效性
|
||
if not use_max_space:
|
||
if size_gb <= 0:
|
||
QMessageBox.warning(self, "输入错误", "逻辑卷大小必须大于0。")
|
||
return None
|
||
# 这里的检查应该使用 self.lv_size_spinbox.maximum() 来判断,因为它是实际的最大值
|
||
if size_gb > self.lv_size_spinbox.maximum():
|
||
QMessageBox.warning(self, "输入错误", "逻辑卷大小不能超过卷组的可用空间。")
|
||
return None
|
||
|
||
return {
|
||
'lv_name': lv_name,
|
||
'vg_name': vg_name,
|
||
'size_gb': size_gb,
|
||
'use_max_space': use_max_space # 返回此标志
|
||
}
|