feat: Add disk management (#10282)

Co-authored-by: wanghe-fit2cloud <wanghe@fit2cloud.com>
This commit is contained in:
CityFun 2025-09-05 17:15:58 +08:00 committed by GitHub
parent 7448953b7d
commit 9c22005b79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1426 additions and 8 deletions

99
agent/app/api/v2/disk.go Normal file
View file

@ -0,0 +1,99 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/gin-gonic/gin"
)
// @Tags Disk Management
// @Summary Get complete disk information
// @Description Get information about all disks including partitioned and unpartitioned disks
// @Produce json
// @Success 200 {object} dto.CompleteDiskInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /disks [get]
func (b *BaseApi) GetCompleteDiskInfo(c *gin.Context) {
diskInfo, err := diskService.GetCompleteDiskInfo()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, diskInfo)
}
// @Tags Disk Management
// @Summary Partition disk
// @Description Create partition and format disk with specified filesystem
// @Accept json
// @Param request body request.DiskPartitionRequest true "partition request"
// @Success 200 {string} string "Partition created successfully"
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /disks/partition [post]
// @x-panel-log {"bodyKeys":["device", "filesystem", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"对磁盘 [device] 进行分区,文件系统 [filesystem],挂载点 [mountPoint]","formatEN":"Partition disk [device] with filesystem [filesystem], mount point [mountPoint]"}
func (b *BaseApi) PartitionDisk(c *gin.Context) {
var req request.DiskPartitionRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
result, err := diskService.PartitionDisk(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Disk Management
// @Summary Mount disk
// @Description Mount partition to specified mount point
// @Accept json
// @Param request body request.DiskMountRequest true "mount request"
// @Success 200 {string} string "Disk mounted successfully"
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /disks/mount [post]
// @x-panel-log {"bodyKeys":["device", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"挂载磁盘 [device] 到 [mountPoint]","formatEN":"Mount disk [device] to [mountPoint]"}
func (b *BaseApi) MountDisk(c *gin.Context) {
var req request.DiskMountRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := diskService.MountDisk(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Disk Management
// @Summary Unmount disk
// @Description Unmount partition from mount point
// @Accept json
// @Param request body request.DiskUnmountRequest true "unmount request"
// @Success 200 {string} string "Disk unmounted successfully"
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /disks/unmount [post]
// @x-panel-log {"bodyKeys":["device", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"卸载磁盘 [device] 从 [mountPoint]","formatEN":"Unmount disk [device] from [mountPoint]"}
func (b *BaseApi) UnmountDisk(c *gin.Context) {
var req request.DiskUnmountRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := diskService.UnmountDisk(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}

View file

@ -71,4 +71,6 @@ var (
taskService = service.NewITaskService()
groupService = service.NewIGroupService()
alertService = service.NewIAlertService()
diskService = service.NewIDiskService()
)

25
agent/app/dto/disk.go Normal file
View file

@ -0,0 +1,25 @@
package dto
type LsblkDevice struct {
Name string `json:"name"`
Size string `json:"size"`
Type string `json:"type"`
MountPoint *string `json:"mountpoint"`
FsType *string `json:"fstype"`
Model *string `json:"model"`
Serial string `json:"serial"`
Tran string `json:"tran"`
Rota bool `json:"rota"`
Children []LsblkDevice `json:"children,omitempty"`
}
type LsblkOutput struct {
BlockDevices []LsblkDevice `json:"blockdevices"`
}
type DiskFormatRequest struct {
Device string `json:"device" `
Filesystem string `json:"filesystem" `
Label string `json:"label,omitempty" `
QuickFormat bool `json:"quickFormat,omitempty"`
}

View file

@ -0,0 +1,19 @@
package request
type DiskPartitionRequest struct {
Device string `json:"device" validate:"required"`
Filesystem string `json:"filesystem" validate:"required,oneof=ext4 xfs"`
Label string `json:"label"`
AutoMount bool `json:"autoMount"`
MountPoint string `json:"mountPoint"`
}
type DiskMountRequest struct {
Device string `json:"device" validate:"required"`
MountPoint string `json:"mountPoint" validate:"required"`
Filesystem string `json:"filesystem" validate:"required,oneof=ext4 xfs"`
}
type DiskUnmountRequest struct {
MountPoint string `json:"mountPoint" validate:"required"`
}

View file

@ -0,0 +1,37 @@
package response
type DiskInfo struct {
DiskBasicInfo
Partitions []DiskBasicInfo `json:"partitions"`
}
type DiskBasicInfo struct {
Device string `json:"device"`
Size string `json:"size"`
Model string `json:"model"`
DiskType string `json:"diskType"`
IsRemovable bool `json:"isRemovable"`
IsSystem bool `json:"isSystem"`
Filesystem string `json:"filesystem"`
Used string `json:"used"`
Avail string `json:"avail"`
UsePercent int `json:"usePercent"`
MountPoint string `json:"mountPoint"`
IsMounted bool `json:"isMounted"`
Serial string `json:"serial"`
}
type CompleteDiskInfo struct {
Disks []DiskInfo `json:"disks"`
UnpartitionedDisks []DiskBasicInfo `json:"unpartitionedDisks"`
SystemDisk *DiskInfo `json:"systemDisk"`
TotalDisks int `json:"totalDisks"`
TotalCapacity int64 `json:"totalCapacity"`
}
type MountInfo struct {
Device string `json:"device"`
MountPoint string `json:"mountPoint"`
Filesystem string `json:"filesystem"`
Options string `json:"options"`
}

554
agent/app/service/disk.go Normal file
View file

@ -0,0 +1,554 @@
package service
import (
"bufio"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
)
type DiskService struct{}
type IDiskService interface {
GetCompleteDiskInfo() (*response.CompleteDiskInfo, error)
PartitionDisk(req request.DiskPartitionRequest) (string, error)
MountDisk(req request.DiskMountRequest) error
UnmountDisk(req request.DiskUnmountRequest) error
}
func NewIDiskService() IDiskService {
return &DiskService{}
}
func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) {
disksWithPartitions, unpartitionedDisks, err := getAllDisksInfo()
if err != nil {
return nil, err
}
var systemDisk *response.DiskInfo
var totalCapacity int64
var filteredDisksWithPartitions []response.DiskInfo
for i, disk := range disksWithPartitions {
if disk.IsSystem {
if systemDisk == nil {
systemDisk = &disksWithPartitions[i]
}
} else {
filteredDisksWithPartitions = append(filteredDisksWithPartitions, disk)
}
}
completeDiskInfo := &response.CompleteDiskInfo{
Disks: filteredDisksWithPartitions,
UnpartitionedDisks: unpartitionedDisks,
SystemDisk: systemDisk,
TotalDisks: len(filteredDisksWithPartitions) + len(unpartitionedDisks),
TotalCapacity: totalCapacity,
}
return completeDiskInfo, nil
}
func getDiskType(rota bool, tran string) string {
if tran == "" {
return ""
}
if !rota {
return "SSD"
}
return "HDD"
}
func getAllDisksInfo() ([]response.DiskInfo, []response.DiskBasicInfo, error) {
var disksWithPartitions []response.DiskInfo
var unpartitionedDisks []response.DiskBasicInfo
output, err := cmd.RunDefaultWithStdoutBashC("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA")
if err != nil {
return nil, nil, err
}
var lsblkOutput dto.LsblkOutput
if err = json.Unmarshal([]byte(output), &lsblkOutput); err != nil {
return nil, nil, err
}
for _, device := range lsblkOutput.BlockDevices {
if device.Type != "disk" {
continue
}
devicePath := "/dev/" + device.Name
model := ""
isPhysical := false
if device.Tran != "" {
isPhysical = true
if device.Model != nil {
model = *device.Model
}
}
hasPartitions := len(device.Children) > 0
if hasPartitions {
disk := response.DiskInfo{
DiskBasicInfo: response.DiskBasicInfo{
Device: devicePath,
Size: device.Size,
DiskType: getDiskType(device.Rota, device.Tran),
IsRemovable: isRemovableDisk(devicePath),
IsSystem: false,
},
Partitions: []response.DiskBasicInfo{},
}
if isPhysical {
disk.Serial = device.Serial
disk.Model = model
}
for _, partition := range device.Children {
partitionInfo := processPartition(partition)
if partitionInfo != nil {
disk.Partitions = append(disk.Partitions, *partitionInfo)
if partitionInfo.IsSystem {
disk.IsSystem = true
}
}
}
if len(disk.Partitions) > 0 {
disksWithPartitions = append(disksWithPartitions, disk)
}
} else {
if isSystemDiskByDevice(device) {
continue
}
unpartitionedDisk := response.DiskBasicInfo{
Device: devicePath,
Size: device.Size,
Model: model,
Serial: device.Serial,
DiskType: getDiskType(device.Rota, device.Tran),
IsRemovable: isRemovableDisk(devicePath),
}
unpartitionedDisks = append(unpartitionedDisks, unpartitionedDisk)
}
}
return disksWithPartitions, unpartitionedDisks, nil
}
func (s *DiskService) PartitionDisk(req request.DiskPartitionRequest) (string, error) {
if !deviceExists(req.Device) {
return "", buserr.WithName("DeviceNotFound", req.Device)
}
if isDeviceMounted(req.Device) {
return "", buserr.WithName("DeviceIsMounted", req.Device)
}
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10 * time.Second))
if err := cmdMgr.RunBashC(fmt.Sprintf("partprobe %s", req.Device)); err != nil {
return "", buserr.WithErr("PartitionDiskErr", err)
}
if err := cmdMgr.RunBashC(fmt.Sprintf("parted -s %s mklabel gpt", req.Device)); err != nil {
return "", buserr.WithErr("PartitionDiskErr", err)
}
if err := cmdMgr.RunBashC(fmt.Sprintf("parted -s %s mkpart primary 1MiB 100%%", req.Device)); err != nil {
return "", buserr.WithErr("PartitionDiskErr", err)
}
if err := cmdMgr.RunBashC(fmt.Sprintf("partprobe %s", req.Device)); err != nil {
return "", buserr.WithErr("PartitionDiskErr", err)
}
partition := req.Device + "1"
formatReq := dto.DiskFormatRequest{
Device: partition,
Filesystem: req.Filesystem,
Label: req.Label,
}
if err := formatDisk(formatReq); err != nil {
return "", buserr.WithErr("FormatDiskErr", err)
}
if req.AutoMount && req.MountPoint != "" {
mountReq := request.DiskMountRequest{
Device: partition,
MountPoint: req.MountPoint,
Filesystem: req.Filesystem,
}
if err := s.MountDisk(mountReq); err != nil {
return "", buserr.WithErr("MountDiskErr", err)
}
}
return partition, nil
}
func (s *DiskService) MountDisk(req request.DiskMountRequest) error {
if !deviceExists(req.Device) {
return buserr.WithName("DeviceNotFound", req.Device)
}
if err := os.MkdirAll(req.MountPoint, 0755); err != nil {
return err
}
fileSystem, err := getFilesystemType(req.Device)
if err != nil {
return err
}
if fileSystem == "" {
formatReq := dto.DiskFormatRequest{
Device: req.Device,
Filesystem: req.Filesystem,
}
if err := formatDisk(formatReq); err != nil {
return buserr.WithErr("FormatDiskErr", err)
}
}
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Minute))
if err := cmdMgr.RunBashC(fmt.Sprintf("mount -t %s %s %s", req.Filesystem, req.Device, req.MountPoint)); err != nil {
return buserr.WithErr("MountDiskErr", err)
}
if err := addToFstabWithOptions(req.Device, req.MountPoint, req.Filesystem, ""); err != nil {
return buserr.WithErr("MountDiskErr", err)
}
return nil
}
func (s *DiskService) UnmountDisk(req request.DiskUnmountRequest) error {
if !isPointMounted(req.MountPoint) {
return buserr.New("MountDiskErr")
}
if err := cmd.RunDefaultBashC(fmt.Sprintf("umount -f %s", req.MountPoint)); err != nil {
return buserr.WithErr("MountDiskErr", err)
}
if err := removeFromFstab(req.MountPoint); err != nil {
global.LOG.Errorf("remove %s mountPoint err: %v", req.MountPoint, err)
}
return nil
}
func formatDisk(req dto.DiskFormatRequest) error {
var mkfsCmd *exec.Cmd
switch req.Filesystem {
case "ext4":
mkfsCmd = exec.Command("mkfs.ext4", "-F", req.Device)
case "xfs":
if !cmd.Which("mkfs.xfs") {
return buserr.New("XfsNotFound")
}
mkfsCmd = exec.Command("mkfs.xfs", "-f", req.Device)
default:
return fmt.Errorf("unsupport type: %s", req.Filesystem)
}
if err := mkfsCmd.Run(); err != nil {
return err
}
return nil
}
func parseDiskInfoLinux(fields []string) (response.DiskInfo, error) {
device := fields[0]
filesystem := fields[1]
sizeStr := fields[2]
usedStr := fields[3]
availStr := fields[4]
usePercentStr := fields[5]
mountPoint := fields[6]
usePercent := 0
if strings.HasSuffix(usePercentStr, "%") {
if percent, err := strconv.Atoi(strings.TrimSuffix(usePercentStr, "%")); err == nil {
usePercent = percent
}
}
return response.DiskInfo{
DiskBasicInfo: response.DiskBasicInfo{
Device: device,
Filesystem: filesystem,
Size: sizeStr,
Used: usedStr,
Avail: availStr,
UsePercent: usePercent,
MountPoint: mountPoint,
},
}, nil
}
func deviceExists(device string) bool {
_, err := os.Stat(device)
return err == nil
}
func isDeviceMounted(device string) bool {
file, err := os.Open("/proc/mounts")
if err != nil {
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) >= 2 && fields[0] == device {
return true
}
}
return false
}
func isPointMounted(mountPoint string) bool {
file, err := os.Open("/proc/mounts")
if err != nil {
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) >= 2 && fields[1] == mountPoint {
return true
}
}
return false
}
func addToFstabWithOptions(device, mountPoint, filesystem, options string) error {
if filesystem == "" {
fsType, err := getFilesystemType(device)
if err != nil {
filesystem = "auto"
} else {
filesystem = fsType
}
}
if options == "" {
options = "defaults"
}
entry := fmt.Sprintf("%s %s %s %s 0 2\n", device, mountPoint, filesystem, options)
file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(entry)
return err
}
func removeFromFstab(mountPoint string) error {
file, err := os.Open("/etc/fstab")
if err != nil {
return err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) >= 2 && fields[1] == mountPoint {
continue
}
lines = append(lines, line)
}
return os.WriteFile("/etc/fstab", []byte(strings.Join(lines, "\n")+"\n"), 0644)
}
func getFilesystemType(device string) (string, error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("blkid -o value -s TYPE %s", device))
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
func isSystemDisk(mountPoint string) bool {
systemMountPoints := []string{
"/",
"/boot",
"/boot/efi",
"/usr",
"/var",
"/home",
}
for _, sysMount := range systemMountPoints {
if mountPoint == sysMount {
return true
}
}
return false
}
func isSystemDiskByDevice(device dto.LsblkDevice) bool {
for _, child := range device.Children {
if child.Type == "part" {
if child.MountPoint != nil && isSystemDisk(*child.MountPoint) {
return true
}
for _, lvmChild := range child.Children {
if lvmChild.Type == "lvm" && lvmChild.MountPoint != nil {
if isSystemDisk(*lvmChild.MountPoint) {
return true
}
}
}
}
}
return false
}
func isRemovableDisk(device string) bool {
deviceName := strings.TrimPrefix(device, "/dev/")
for i := len(deviceName) - 1; i >= 0; i-- {
if deviceName[i] < '0' || deviceName[i] > '9' {
deviceName = deviceName[:i+1]
break
}
}
removablePath := filepath.Join("/sys/block", deviceName, "removable")
if data, err := os.ReadFile(removablePath); err == nil {
removable := strings.TrimSpace(string(data))
return removable == "1"
}
return false
}
func processPartition(partition dto.LsblkDevice) *response.DiskBasicInfo {
if partition.Type != "part" {
return nil
}
devicePath := "/dev/" + partition.Name
var mountPoint, filesystem string
if partition.MountPoint != nil {
mountPoint = *partition.MountPoint
}
if partition.FsType != nil {
filesystem = *partition.FsType
}
var (
actualMountPoint, actualFilesystem string
size, used, avail string
usePercent int
isMounted bool
isSystem bool
)
if len(partition.Children) > 0 {
for _, child := range partition.Children {
if child.Type == "lvm" {
lvmDevicePath := "/dev/mapper/" + child.Name
if child.MountPoint != nil {
actualMountPoint = *child.MountPoint
isMounted = true
}
if child.FsType != nil {
actualFilesystem = *child.FsType
}
if actualMountPoint != "" {
diskUsage, err := getDiskUsageInfo(lvmDevicePath)
if err == nil {
used = diskUsage.Used
avail = diskUsage.Avail
size = diskUsage.Size
usePercent = diskUsage.UsePercent
}
}
isSystem = isSystemDisk(actualMountPoint)
break
}
}
} else {
actualMountPoint = mountPoint
actualFilesystem = filesystem
isMounted = mountPoint != ""
if actualMountPoint != "" {
diskUsage, err := getDiskUsageInfo(devicePath)
if err == nil {
used = diskUsage.Used
avail = diskUsage.Avail
size = diskUsage.Size
usePercent = diskUsage.UsePercent
}
}
isSystem = isSystemDisk(actualMountPoint)
}
if size == "" {
size = partition.Size
}
return &response.DiskBasicInfo{
Device: devicePath,
Size: size,
Used: used,
Avail: avail,
UsePercent: usePercent,
MountPoint: actualMountPoint,
Filesystem: actualFilesystem,
IsMounted: isMounted,
IsSystem: isSystem,
}
}
func getDiskUsageInfo(device string) (response.DiskInfo, error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -hT -P %s", device))
if err != nil {
return response.DiskInfo{}, err
}
lines := strings.Split(output, "\n")
if len(lines) < 2 {
return response.DiskInfo{}, err
}
fields := strings.Fields(lines[1])
if len(fields) < 7 {
return response.DiskInfo{}, err
}
return parseDiskInfoLinux(fields)
}

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. P
CommonAlert: "Your 1Panel, {{ .msg }}, please log in to the panel for details."
NodeExceptionAlert: "Your 1Panel, {{ .num }} nodes are abnormal, please log in to the panel for details."
LicenseExceptionAlert: "Your 1Panel, {{ .num }} licenses are abnormal, please log in to the panel for details."
SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details."
SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details."
#disk
DeviceNotFound: "Device {{ .name }} not found"
DeviceIsMounted: "Device {{ .name }} is mounted, please unmount first"
PartitionDiskErr: "Failed to partition, {{ .err }}"
FormatDiskErr: "Failed to format disk, {{ .err }}"
MountDiskErr: "Failed to mount disk, {{ .err }}"
UnMountDiskErr: "Failed to unmount disk, {{ .err }}"
XfsNotFound: "xfs filesystem not detected, please install xfsprogs first"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期
CommonAlert: "お使いの1Panel、{{ .msg }}、詳細はパネルにログインしてご確認ください。"
NodeExceptionAlert: "お使いの1Panel、{{ .num }}個のノードに異常があります。詳細はパネルにログインしてご確認ください。"
LicenseExceptionAlert: "お使いの1Panel、{{ .num }}個のライセンスに異常があります。詳細はパネルにログインしてご確認ください。"
SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。"
SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。"
#disk
DeviceNotFound: "デバイス {{ .name }} が見つかりません"
DeviceIsMounted: "デバイス {{ .name }} はマウントされています、まずアンマウントしてください"
PartitionDiskErr: "パーティションに失敗しました、{{ .err }}"
FormatDiskErr: "ディスクのフォーマットに失敗しました、{{ .err }}"
MountDiskErr: "ディスクのマウントに失敗しました、{{ .err }}"
UnMountDiskErr: "ディスクのアンマウントに失敗しました、{{ .err }}"
XfsNotFound: "xfs ファイルシステムが検出されませんでした、最初に xfsprogs をインストールしてください"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩
CommonAlert: "귀하의 1Panel, {{ .msg }}. 자세한 내용은 패널에 로그인하여 확인하세요."
NodeExceptionAlert: "귀하의 1Panel, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
LicenseExceptionAlert: "귀하의 1Panel, {{ .num }}개의 라이선스에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
#disk
DeviceNotFound: "장치 {{ .name }} 을(를) 찾을 수 없습니다"
DeviceIsMounted: "장치 {{ .name }} 이(가) 마운트되었습니다, 먼저 마운트 해제하세요"
PartitionDiskErr: "파티션 분할에 실패했습니다, {{ .err }}"
FormatDiskErr: "디스크 포맷에 실패했습니다, {{ .err }}"
MountDiskErr: "디스크 마운트에 실패했습니다, {{ .err }}"
UnMountDiskErr: "디스크 마운트 해제에 실패했습니다, {{ .err }}"
XfsNotFound: "xfs 파일 시스템이 감지되지 않았습니다, 먼저 xfsprogs 를 설치하세요"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} ha
CommonAlert: "1Panel anda, {{ .msg }}, sila log masuk ke panel untuk maklumat lanjut."
NodeExceptionAlert: "1Panel anda, {{ .num }} nod bermasalah, sila log masuk ke panel untuk maklumat lanjut."
LicenseExceptionAlert: "1Panel anda, {{ .num }} lesen bermasalah, sila log masuk ke panel untuk maklumat lanjut."
SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut."
SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut."
#disk
DeviceNotFound: "Peranti {{ .name }} tidak ditemui"
DeviceIsMounted: "Peranti {{ .name }} telah dikaitkan, sila nyahkaitkan terlebih dahulu"
PartitionDiskErr: "Gagal membahagikan, {{ .err }}"
FormatDiskErr: "Gagal memformat cakera, {{ .err }}"
MountDiskErr: "Gagal mengkaitkan cakera, {{ .err }}"
UnMountDiskErr: "Gagal nyahkaitkan cakera, {{ .err }}"
XfsNotFound: "Sistem fail xfs tidak dikesan, sila pasang xfsprogs terlebih dahulu"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse
CommonAlert: "Seu 1Panel, {{ .msg }}. Para mais detalhes, faça login no painel."
NodeExceptionAlert: "Seu 1Panel, {{ .num }} nós estão com problemas. Para mais detalhes, faça login no painel."
LicenseExceptionAlert: "Seu 1Panel, {{ .num }} licenças estão com problemas. Para mais detalhes, faça login no painel."
SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel."
SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel."
#disk
DeviceNotFound: "Dispositivo {{ .name }} não encontrado"
DeviceIsMounted: "Dispositivo {{ .name }} está montado, por favor desmonte primeiro"
PartitionDiskErr: "Falha ao particionar, {{ .err }}"
FormatDiskErr: "Falha ao formatar disco, {{ .err }}"
MountDiskErr: "Falha ao montar disco, {{ .err }}"
UnMountDiskErr: "Falha ao desmontar disco, {{ .err }}"
XfsNotFound: "Sistema de arquivos xfs não detectado, por favor instale xfsprogs primeiro"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Пароль для 1Panel истекает через
CommonAlert: "Ваш 1Panel, {{ .msg }}. Подробности смотрите, войдя в панель."
NodeExceptionAlert: "Ваш 1Panel, {{ .num }} узлов работают неправильно. Подробности смотрите, войдя в панель."
LicenseExceptionAlert: "Ваш 1Panel, {{ .num }} лицензий работают неправильно. Подробности смотрите, войдя в панель."
SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель."
SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель."
#disk
DeviceNotFound: "Устройство {{ .name }} не найдено"
DeviceIsMounted: "Устройство {{ .name }} подключено, сначала отключите"
PartitionDiskErr: "Не удалось разделить, {{ .err }}"
FormatDiskErr: "Не удалось отформатировать диск, {{ .err }}"
MountDiskErr: "Не удалось подключить диск, {{ .err }}"
UnMountDiskErr: "Не удалось отключить диск, {{ .err }}"
XfsNotFound: "Файловая система xfs не обнаружена, сначала установите xfsprogs"

View file

@ -476,3 +476,12 @@ CommonAlert: "1Panel'iniz, {{ .msg }}. Detaylar için panele giriş yapınız."
NodeExceptionAlert: "1Panel'iniz, {{ .num }} düğümde sorun var. Detaylar için panele giriş yapınız."
LicenseExceptionAlert: "1Panel'iniz, {{ .num }} lisansda sorun var. Detaylar için panele giriş yapınız."
SSHAndPanelLoginAlert: "1Panel'iniz, {{ .ip }} adresinden {{ .name }} paneline anormal giriş tespit edildi. Detaylar için panele giriş yapınız."
#disk
DeviceNotFound: "Cihaz {{ .name }} bulunamadı"
DeviceIsMounted: "Cihaz {{ .name }} bağlandı, önce çıkarın"
PartitionDiskErr: "Bölümleme başarısız, {{ .err }}"
FormatDiskErr: "Disk biçimlendirme başarısız, {{ .err }}"
MountDiskErr: "Disk bağlama başarısız, {{ .err }}"
UnMountDiskErr: "Disk bağının kaldırılması başarısız, {{ .err }}"
XfsNotFound: "XFS dosya sistemi algılanmadı, lütfen önce xfsprogs'i yükleyin"

View file

@ -474,3 +474,12 @@ CommonAlert: "您的 1Panel 面板,{{ .msg }},詳情請登入面板查看。
NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 個節點存在異常,詳情請登入面板查看。"
LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 個許可證存在異常,詳情請登入面板查看。"
SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}從 {{ .ip }} 登錄異常,詳情請登入面板查看。"
#disk
DeviceNotFound: "設備 {{ .name }} 未找到"
DeviceIsMounted: "設備 {{ .name }} 已掛載,請先卸載"
PartitionDiskErr: "分區失敗,{{ .err }}"
FormatDiskErr: "格式化磁盤失敗,{{ .err }}"
MountDiskErr: "掛載磁盤失敗,{{ .err }}"
UnMountDiskErr: "卸載磁盤失敗,{{ .err }}"
XfsNotFound: "未檢測到 xfs 文件系統,請先安裝 xfsprogs"

View file

@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }}
CommonAlert: "您的 1Panel 面板,{{ .msg }},详情请登录面板查看。"
NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 个节点存在异常,详情请登录面板查看。"
LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 个许可证存在异常,详情请登录面板查看。"
SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。"
SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。"
#disk
DeviceNotFound: "设备 {{ .name }} 未找到"
DeviceIsMounted: "设备 {{ .name }} 已挂载,请先卸载"
PartitionDiskErr: "分区失败,{{ .err }}"
FormatDiskErr: "格式化磁盘失败,{{ .err }}"
MountDiskErr: "挂载磁盘失败,{{ .err }}"
UnMountDiskErr: "卸载磁盘失败,{{ .err }}"
XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs"

View file

@ -52,5 +52,10 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile)
hostRouter.GET("/terminal", baseApi.WsSSH)
hostRouter.GET("/disks", baseApi.GetCompleteDiskInfo)
hostRouter.POST("/disks/partition", baseApi.PartitionDisk)
hostRouter.POST("/disks/mount", baseApi.MountDisk)
hostRouter.POST("/disks/unmount", baseApi.UnmountDisk)
}
}

View file

@ -21,6 +21,7 @@ func Init() {
migrations.AddClusterMenu,
migrations.DeleteXpackHideMenu,
migrations.AddCronjobGroup,
migrations.AddDiskMenu,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -511,3 +511,17 @@ var AddCronjobGroup = &gormigrate.Migration{
return nil
},
}
var AddDiskMenu = &gormigrate.Migration{
ID: "20250811-add-disk-menu",
Migrate: func(tx *gorm.DB) error {
return helper.AddMenu(dto.ShowMenu{
ID: "77",
Disabled: false,
Title: "menu.disk",
IsShow: true,
Label: "Disk",
Path: "/hosts/disk",
}, "7", tx)
},
}

View file

@ -204,4 +204,50 @@ export namespace Host {
status: string;
message: string;
}
export interface DiskBasicInfo {
device: string;
size: string;
model: string;
diskType: string;
isRemovable: boolean;
isSystem: boolean;
filesystem: string;
used: string;
avail: string;
usePercent: number;
mountPoint: string;
isMounted: boolean;
serial: string;
}
export interface DiskInfo extends DiskBasicInfo {
partitions?: DiskBasicInfo[];
}
export interface CompleteDiskInfo {
disks: DiskInfo[];
unpartitionedDisks: DiskBasicInfo[];
systemDisk?: DiskInfo;
totalDisks: number;
totalCapacity: number;
}
export interface DiskPartition {
device: string;
filesystem: string;
label: string;
autoMount: boolean;
mountPoint: string;
}
export interface DiskMount {
device: string;
mountPoint: string;
filesystem?: string;
}
export interface DiskUmount {
mountPoint: string;
}
}

View file

@ -106,5 +106,21 @@ export const loadSSHLogs = (params: Host.searchSSHLog) => {
return http.post<ResPage<Host.sshHistory>>(`/hosts/ssh/log`, params);
};
export const exportSSHLogs = (params: Host.searchSSHLog) => {
return http.post<string>('/hosts/ssh/log/export', params, TimeoutEnum.T_40S);
return http.post<string>(`/hosts/ssh/log/export`, params, TimeoutEnum.T_40S);
};
export const listDisks = () => {
return http.get<Host.CompleteDiskInfo>(`/hosts/disks`);
};
export const partitionDisk = (params: Host.DiskPartition) => {
return http.post(`/hosts/disks/partition`, params, TimeoutEnum.T_60S);
};
export const mountDisk = (params: Host.DiskMount) => {
return http.post(`/hosts/disks/mount`, params, TimeoutEnum.T_60S);
};
export const unmountDisk = (params: Host.DiskUmount) => {
return http.post(`/hosts/disks/unmount`, params, TimeoutEnum.T_60S);
};

View file

@ -2927,6 +2927,27 @@ const message = {
autoStartHelper: 'Whether to automatically start the service after Supervisor starts',
},
},
disk: {
management: 'Disk Management',
partition: 'Partition',
unmount: 'Unmount',
unmountHelper: 'Do you want to unmount the partition {0}?',
mount: 'Mount',
partitionAlert:
'Disk partitioning requires formatting the disk, and existing data will be deleted. Please save or take snapshots of your data in advance.',
mountPoint: 'Mount Directory',
systemDisk: 'System Disk',
unpartitionedDisk: 'Unpartitioned Disk',
handlePartition: 'Partition Now',
filesystem: 'Filesystem',
unmounted: 'Unmounted',
cannotOperate: 'Cannot Operate',
systemDiskHelper: 'Hint: The current disk is the system disk. It cannot be operated on.',
autoMount: 'Auto Mount',
model: 'Device Model',
diskType: 'Disk Type',
serial: 'Serial Number',
},
xpack: {
expiresTrialAlert:
'Friendly reminder: Your Pro trial will expire in {0} days, and all Pro features will no longer be accessible. Please renew or upgrade to the full version in a timely manner.',

View file

@ -2822,6 +2822,27 @@ const message = {
autoStartHelper: 'Supervisor 起動後にサービスを自動的に起動するかどうか',
},
},
disk: {
management: 'ディスク管理',
partition: 'パーティション',
unmount: 'アンマウント',
unmountHelper: 'パーティション {0} をアンマウントしますか',
mount: 'マウント',
partitionAlert:
'ディスクのパーティション分割にはディスクのフォーマットが必要で既存のデータは削除されます事前にデータを保存またはスナップショットを取ってください',
mountPoint: 'マウントディレクトリ',
systemDisk: 'システムディスク',
unpartitionedDisk: '未パーティションディスク',
handlePartition: '今すぐパーティション',
filesystem: 'ファイルシステム',
unmounted: 'アンマウント',
cannotOperate: '操作不可',
systemDiskHelper: 'ヒント: 現在のディスクはシステムディスクです操作できません',
autoMount: '自動マウント',
model: 'デバイスモデル',
diskType: 'ディスクタイプ',
serial: 'シリアルナンバー',
},
xpack: {
expiresTrialAlert:
'ご注意: あなたのProトライアルは{0}日後に終了しすべてのPro機能が使用できなくなります適時に更新またはフルバージョンにアップグレードしてください',

View file

@ -2773,6 +2773,27 @@ const message = {
autoStartHelper: 'Supervisor 시작 서비스를 자동으로 시작할지 여부',
},
},
disk: {
management: '디스크 관리',
partition: '파티션',
unmount: '마운트 해제',
unmountHelper: '파티션 {0} () 마운트 해제하시겠습니까?',
mount: '마운트',
partitionAlert:
'디스크 파티션 작업은 디스크 포맷이 필요하며, 기존 데이터는 삭제됩니다. 데이터를 미리 저장하거나 스냅샷을 찍어주세요.',
mountPoint: '마운트 디렉토리',
systemDisk: '시스템 디스크',
unpartitionedDisk: '미파티션 디스크',
handlePartition: '지금 파티션',
filesystem: '파일 시스템',
unmounted: '마운트 해제됨',
cannotOperate: '작업 불가',
systemDiskHelper: '힌트: 현재 디스크는 시스템 디스크입니다. 작업할 없습니다.',
autoMount: '자동 마운트',
model: '장치 모델',
diskType: '디스크 유형',
serial: '시리얼 번호',
},
xpack: {
expiresTrialAlert:
'친절한 알림: 귀하의 Pro 체험판이 {0} 만료되며, 모든 Pro 기능에 이상 접근할 없습니다. 제때 갱신하거나 전체 버전으로 업그레이드하시기 바랍니다.',

View file

@ -2886,6 +2886,19 @@ const message = {
autoStartHelper: 'Sama ada untuk memulakan perkhidmatan secara automatik selepas Supervisor mula',
},
},
disk: {
systemDisk: 'Cakera Sistem',
unpartitionedDisk: 'Cakera Tidak Dibahagikan',
handlePartition: 'Bahagikan Sekarang',
filesystem: 'Sistem Fail',
unmounted: 'Tidak Dikaitkan',
cannotOperate: 'Tidak Boleh Beroperasi',
systemDiskHelper: 'Petunjuk: Cakera semasa adalah cakera sistem, tidak boleh dioperasikan.',
autoMount: 'Pemasangan Automatik',
model: 'Model Peranti',
diskType: 'Jenis Cakera',
serial: 'Nombor Siri',
},
xpack: {
expiresTrialAlert:
'Peringatan mesra: Percubaan Pro anda akan tamat dalam {0} hari, dan semua ciri Pro tidak lagi dapat diakses. Sila perbaharui atau naik taraf ke versi penuh tepat pada masanya.',

View file

@ -2889,6 +2889,27 @@ const message = {
autoStartHelper: 'Se o serviço deve ser iniciado automaticamente após o Supervisor iniciar',
},
},
disk: {
management: 'Gerenciamento de Disco',
partition: 'Partição',
unmount: 'Desmontar',
unmountHelper: 'Deseja desmontar a partição {0}?',
mount: 'Montar',
partitionAlert:
'O particionamento de disco requer formatação do disco, e os dados existentes serão excluídos. Salve ou tire snapshots dos dados com antecedência.',
mountPoint: 'Diretório de Montagem',
systemDisk: 'Disco do Sistema',
unpartitionedDisk: 'Disco Não Particionado',
handlePartition: 'Particionar Agora',
filesystem: 'Sistema de Arquivos',
unmounted: 'Desmontado',
cannotOperate: 'Não Pode Operar',
systemDiskHelper: 'Dica: O disco atual é o disco do sistema, não pode ser operado.',
autoMount: 'Montagem Automática',
model: 'Modelo do Dispositivo',
diskType: 'Tipo de Disco',
serial: 'Número de Série',
},
xpack: {
expiresTrialAlert:
'Lembrete: Sua versão de avaliação profissional expirará em {0} dias. Após isso, todas as funcionalidades da versão profissional não estarão mais disponíveis. Por favor, renove ou faça upgrade para a versão oficial a tempo.',

View file

@ -2881,6 +2881,27 @@ const message = {
autoStartHelper: 'Автоматически запускать сервис после запуска Supervisor',
},
},
disk: {
management: 'Управление дисками',
partition: 'Раздел',
unmount: 'Отмонтировать',
unmountHelper: 'Вы хотите отмонтировать раздел {0}?',
mount: 'Подключить',
partitionAlert:
'Разделение диска требует форматирования диска, и существующие данные будут удалены. Пожалуйста, сохраните или сделайте снимки данных заранее.',
mountPoint: 'Точка монтирования',
systemDisk: 'Системный диск',
unpartitionedDisk: 'Неразделенный диск',
handlePartition: 'Разделить сейчас',
filesystem: 'Файловая система',
unmounted: 'Отмонтирован',
cannotOperate: 'Невозможно выполнить операцию',
systemDiskHelper: 'Подсказка: Текущий диск является системным диском, операции невозможны.',
autoMount: 'Автоматическое монтирование',
model: 'Модель устройства',
diskType: 'Тип диска',
serial: 'Серийный номер',
},
xpack: {
expiresTrialAlert:
'Дружеское напоминание: ваша пробная версия Pro истечет через {0} дней, и все функции Pro станут недоступны. Пожалуйста, своевременно продлите или обновите до полной версии.',

View file

@ -2964,6 +2964,27 @@ const message = {
autoStartHelper: 'Supervisor başlatıldıktan sonra servis otomatik olarak başlatılsın mı?',
},
},
disk: {
management: 'Disk Yönetimi',
partition: 'Bölüm',
unmount: 'Bağını Kaldır',
unmountHelper: "Bölüm {0}'ın bağını kaldırmak istiyor musunuz?",
mount: 'Bağla',
partitionAlert:
'Disk bölümleme, diske biçimlendirme gerektirir ve mevcut veriler silinir. Lütfen verilerinizi önceden kaydedin veya anlık görüntü alın.',
mountPoint: 'Bağlama Noktası',
systemDisk: 'Sistem Diski',
unpartitionedDisk: 'Bölümlendirilmemiş Disk',
handlePartition: 'Şimdi Bölümle',
filesystem: 'Dosya Sistemi',
unmounted: 'Bağlı Değil',
cannotOperate: 'Operasyon Yapılamıyor',
systemDiskHelper: 'İpucu: Mevcut disk sistem diskidir, işlem yapılamaz.',
autoMount: 'Otomatik Bağlama',
model: 'Cihaz Modeli',
diskType: 'Disk Türü',
serial: 'Seri Numarası',
},
xpack: {
expiresTrialAlert:
'Nazik hatırlatma: Pro deneme sürümünüz {0} gün içinde sona erecek ve tüm Pro özellikleri kullanılamaz hale gelecektir. Lütfen zamanında yenileyin veya tam sürüme yükseltin.',

View file

@ -2727,6 +2727,26 @@ const message = {
autoStartHelper: 'Supervisor 啟動後是否自動啟動服務',
},
},
disk: {
management: '磁盤管理',
partition: '分區',
unmount: '取消掛載',
unmountHelper: '是否取消掛載分區 {0}',
mount: '掛載',
partitionAlert: '進行磁盤分區時需要格式化磁盤原有數據將被刪除請提前保存或快照數據',
mountPoint: '掛載目錄',
systemDisk: '系統磁盤',
unpartitionedDisk: '未分區磁盤',
handlePartition: '立即分區',
filesystem: '文件系統',
unmounted: '未掛載',
cannotOperate: '無法操作',
systemDiskHelper: '提示:當前磁盤為系統盤無法進行操作',
autoMount: '自動掛載',
model: '設備型號',
diskType: '磁盤類型',
serial: '序列號',
},
xpack: {
expiresTrialAlert:
'溫馨提醒您的專業版試用將在 {0} 天後到期屆時所有專業版功能將無法繼續使用請及時續費或升級到正式版本',

View file

@ -370,6 +370,7 @@ const message = {
tamper: '防篡改',
app: '应用',
msgCenter: '任务中心',
disk: '磁盘管理',
},
home: {
recommend: '推荐',
@ -2718,6 +2719,26 @@ const message = {
autoStartHelper: 'Supervisor 启动后是否自动启动服务',
},
},
disk: {
management: '磁盘管理',
partition: '分区',
unmount: '取消挂载',
unmountHelper: '是否取消挂载分区 {0}',
mount: '挂载',
partitionAlert: '进行磁盘分区时需要格式化磁盘原有数据将被删除请提前保存或快照数据',
mountPoint: '挂载目录',
systemDisk: '系统磁盘',
unpartitionedDisk: '未分区磁盘',
handlePartition: '立即分区',
filesystem: '文件系统',
unmounted: '未挂载',
cannotOperate: '无法操作',
systemDiskHelper: '提示:当前磁盘为系统盘无法进行操作',
autoMount: '自动挂载',
model: '设备型号',
diskType: '磁盘类型',
serial: '序列号',
},
xpack: {
expiresAlert: '温馨提醒专业版试用将于 [{0}] 天后到期届时将停止使用所有专业版功能',
name: '专业版',

View file

@ -137,6 +137,16 @@ const hostRouter = {
requiresAuth: false,
},
},
{
path: '/hosts/disk',
name: 'Disk',
props: true,
component: () => import('@/views/host/disk-management/disk/index.vue'),
meta: {
title: 'menu.disk',
requiresAuth: false,
},
},
],
};

View file

@ -0,0 +1,133 @@
<template>
<el-card class="shadow-sm">
<div class="border-b pb-4">
<div class="flex items-center space-x-4">
<div>
<h3 class="text-lg">
{{ $t('home.disk') }}{{ $t('commons.table.name') }}: {{ diskInfo.device }}
<el-tag size="small" type="warning" v-if="scope === 'system'">
{{ $t('disk.systemDisk') }}
</el-tag>
<el-tag size="small" type="warning" v-if="scope === 'unpartitioned'">
{{ $t('disk.unpartitionedDisk') }}
</el-tag>
</h3>
<div class="flex items-center space-x-6 text-sm">
<el-text type="info">{{ $t('container.size') }}: {{ diskInfo.size }}</el-text>
<el-text type="info">
{{ $t('disk.partition') }}:
<span v-if="diskInfo.partitions">
{{ diskInfo.partitions?.length }}
</span>
<span v-else>0</span>
</el-text>
<el-text type="info" v-if="diskInfo.diskType" class="flex items-center">
{{ $t('disk.diskType') }}:
<el-tag class="ml-2" size="small" type="info">{{ diskInfo.diskType }}</el-tag>
</el-text>
<el-text type="info" v-if="diskInfo.model" class="flex items-center">
{{ $t('disk.model') }}:
<span class="ml-2">{{ diskInfo.model }}</span>
</el-text>
<el-text type="info" v-if="diskInfo.serial" class="flex items-center">
{{ $t('disk.model') }}:
<span class="ml-2">{{ diskInfo.serial }}</span>
</el-text>
<div v-if="scope == 'unpartitioned'">
<el-button type="primary" size="small" @click="handlePartition(diskInfo)">
{{ $t('disk.handlePartition') }}
</el-button>
</div>
</div>
</div>
</div>
</div>
<div v-if="diskInfo.partitions && diskInfo.partitions.length > 0">
<el-table :data="diskInfo.partitions" class="w-full">
<el-table-column prop="device" :label="$t('disk.partition') + $t('commons.table.name')" min-width="100">
<template #default="{ row }">
<span class="font-medium">{{ row.device.split('/').pop() }}</span>
</template>
</el-table-column>
<el-table-column prop="size" :label="$t('container.size')" min-width="40" />
<el-table-column prop="used" :label="$t('home.used')" min-width="40" />
<el-table-column prop="avail" :label="$t('home.available')" min-width="40" />
<el-table-column prop="usePercent" :label="$t('home.percent')" min-width="60">
<template #default="{ row }">
<el-progress
:percentage="row.usePercent"
:status="row.usePercent >= 90 ? 'exception' : 'success'"
:text-inside="true"
:stroke-width="14"
/>
</template>
</el-table-column>
<el-table-column prop="mountPoint" :label="$t('disk.mountPoint')" min-width="120">
<template #default="{ row }">
<span v-if="row.mountPoint != ''">
{{ row.mountPoint }}
</span>
<el-tag v-else size="small" type="warning">{{ $t('disk.unmounted') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="filesystem" :label="$t('disk.filesystem')" min-width="80">
<template #default="{ row }">
<el-tag size="small" type="info" v-if="row.filesystem != ''">{{ row.filesystem }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" width="150">
<template #default="{ row }">
<el-text type="info" v-if="scope === 'system'">{{ $t('disk.cannotOperate') }}</el-text>
<el-button type="primary" link v-else-if="row.mountPoint != ''" @click="unmount(row)">
{{ $t('disk.unmount') }}
</el-button>
<el-button type="primary" link v-else @click="mount(row)">{{ $t('disk.mount') }}</el-button>
</template>
</el-table-column>
</el-table>
<el-text v-if="scope === 'system'">{{ $t('disk.systemDiskHelper') }}</el-text>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { Host } from '@/api/interface/host';
import i18n from '@/lang';
import { unmountDisk } from '@/api/modules/host';
import { MsgSuccess } from '@/utils/message';
const emit = defineEmits(['partition', 'search', 'mount']);
defineProps({
diskInfo: {
type: Object as () => Host.DiskInfo,
required: true,
},
scope: {
type: String,
required: false,
},
});
const handlePartition = (diskInfo: Host.DiskInfo) => {
emit('partition', diskInfo);
};
const mount = (diskInfo: Host.DiskInfo) => {
emit('mount', diskInfo);
};
const unmount = (diskInfo: Host.DiskInfo) => {
ElMessageBox.confirm(i18n.global.t('disk.unmountHelper', [diskInfo.device]), i18n.global.t('disk.unmount'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
unmountDisk({
mountPoint: diskInfo.mountPoint,
}).then(() => {
MsgSuccess(i18n.global.t('disk.unmount') + i18n.global.t('commons.status.success'));
emit('search');
});
});
};
</script>

View file

@ -0,0 +1,59 @@
<template>
<div>
<DiskRouter />
<MainDiv class="mt-2" :height-diff="140" v-loading="loading">
<div v-if="diskInfo?.systemDisk">
<DiskCard class="mt-2" :diskInfo="diskInfo.systemDisk" scope="system" />
</div>
<div v-if="diskInfo?.unpartitionedDisks">
<DiskCard
class="mt-2"
v-for="(disk, index) in diskInfo.unpartitionedDisks"
:key="index"
:diskInfo="disk"
scope="unpartitioned"
@partition="(diskInfo) => partitionRef.acceptParams(diskInfo, 'partition')"
@search="() => getDisk()"
/>
</div>
<div v-if="diskInfo?.disks">
<DiskCard
class="mt-2"
v-for="(disk, index) in diskInfo.disks"
:key="index"
:diskInfo="disk"
@mount="(diskInfo) => partitionRef.acceptParams(diskInfo, 'mount')"
scope="normal"
@search="() => getDisk()"
/>
</div>
</MainDiv>
<Partition ref="partitionRef" @search="getDisk" />
</div>
</template>
<script lang="ts" setup>
import { Host } from '@/api/interface/host';
import { listDisks } from '@/api/modules/host';
import DiskRouter from '@/views/host/disk-management/index.vue';
import DiskCard from '@/views/host/disk-management/components/disk-card.vue';
import Partition from '@/views/host/disk-management/partition/index.vue';
const loading = ref(false);
const partitionRef = ref();
const diskInfo = ref<Host.CompleteDiskInfo>();
const getDisk = async () => {
try {
loading.value = true;
const res = await listDisks();
diskInfo.value = res.data;
} catch (error) {
} finally {
loading.value = false;
}
};
onMounted(() => {
getDisk();
});
</script>

View file

@ -0,0 +1,19 @@
<template>
<div>
<RouterButton :buttons="buttons" />
<LayoutContent>
<router-view></router-view>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
import i18n from '@/lang';
const buttons = [
{
label: i18n.global.t('menu.disk'),
path: '/hosts/disk',
},
];
</script>

View file

@ -0,0 +1,118 @@
<template>
<DrawerPro
v-model="open"
:header="$t('disk.' + operate)"
:resource="form.device"
@close="handleClose"
v-loading="loading"
>
<el-alert
v-if="operate == 'partition' || (operate == 'mount' && filesystem == '')"
:title="$t('disk.partitionAlert')"
type="warning"
:closable="false"
/>
<el-form
@submit.prevent
ref="partitionRef"
:rules="rules"
label-position="top"
:model="form"
class="mt-2"
v-loading="loading"
>
<el-form-item :label="$t('disk.filesystem')" prop="filesystem">
<el-radio-group v-model="form.filesystem" :disabled="operate == 'mount' && filesystem != ''">
<el-radio-button label="ext4" value="ext4" />
<el-radio-button label="xfs" value="xfs" />
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('disk.autoMount')" prop="autoMount">
<el-switch v-model="form.autoMount" />
</el-form-item>
<el-form-item :label="$t('disk.mountPoint')" prop="mountPoint">
<el-input v-model="form.mountPoint">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: true })" />
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
<FileList ref="fileRef" @choose="loadBuildDir" />
</DrawerPro>
</template>
<script lang="ts" setup>
import { Host } from '@/api/interface/host';
import { mountDisk, partitionDisk } from '@/api/modules/host';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const rules = ref<any>({
filesystem: [Rules.requiredInput],
autoMount: [Rules.requiredInput],
mountPoint: [Rules.requiredInput],
});
const form = reactive({
device: '',
filesystem: 'ext4',
autoMount: true,
mountPoint: '',
label: '',
});
const open = ref(false);
const loading = ref(false);
const operate = ref('mount');
const fileRef = ref();
const emit = defineEmits(['search']);
const filesystem = ref('');
const loadBuildDir = async (path: string) => {
form.mountPoint = path;
};
const acceptParams = (diskInfo: Host.DiskInfo, operateType: string) => {
operate.value = operateType;
form.device = diskInfo.device;
form.mountPoint = '';
if (operateType == 'mount' && diskInfo.filesystem) {
filesystem.value = diskInfo.filesystem;
form.filesystem = diskInfo.filesystem;
}
open.value = true;
};
const submit = async () => {
try {
loading.value = true;
if (operate.value == 'mount') {
await mountDisk(form);
MsgSuccess(i18n.global.t('disk.mount') + i18n.global.t('commons.status.success'));
handleClose();
} else {
await partitionDisk(form);
MsgSuccess(i18n.global.t('disk.partition') + i18n.global.t('commons.status.success'));
handleClose();
}
} catch (error) {
loading.value = false;
return;
}
};
const handleClose = () => {
open.value = false;
emit('search');
};
defineExpose({
acceptParams,
});
</script>