feat: Disk Management Compatible with Lower Version Operating Systems (#10298)

This commit is contained in:
CityFun
2025-09-08 17:04:07 +08:00
committed by GitHub
parent f4334eadc0
commit b428e699db
5 changed files with 454 additions and 425 deletions
+9 -419
View File
@@ -1,17 +1,12 @@
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"
@@ -33,127 +28,24 @@ func NewIDiskService() IDiskService {
}
func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) {
disksWithPartitions, unpartitionedDisks, err := getAllDisksInfo()
output, err := cmd.RunDefaultWithStdoutBashC("lsblk -P -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA")
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)
}
return nil, fmt.Errorf("failed to execute lsblk command: %v", err)
}
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")
diskInfos, err := parseLsblkOutput(output)
if err != nil {
return nil, nil, err
return nil, fmt.Errorf("failed to parse lsblk output: %v", 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
result := organizeDiskInfo(diskInfos)
return &result, nil
}
func (s *DiskService) PartitionDisk(req request.DiskPartitionRequest) (string, error) {
if !strings.HasPrefix("/dev", req.Device) {
req.Device = "/dev/" + req.Device
}
if !deviceExists(req.Device) {
return "", buserr.WithName("DeviceNotFound", req.Device)
}
@@ -250,305 +142,3 @@ func (s *DiskService) UnmountDisk(req request.DiskUnmountRequest) error {
}
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)
}
+433
View File
@@ -0,0 +1,433 @@
package service
import (
"bufio"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
func organizeDiskInfo(diskInfos []response.DiskBasicInfo) response.CompleteDiskInfo {
var result response.CompleteDiskInfo
var systemDisk *response.DiskInfo
diskMap := make(map[string]*response.DiskInfo)
partitions := make(map[string][]response.DiskBasicInfo)
for _, info := range diskInfos {
isPartition := isPartitionDevice(info.Device)
if isPartition {
parentDevice := getParentDevice(info.Device)
partitions[parentDevice] = append(partitions[parentDevice], info)
} else {
disk := &response.DiskInfo{
DiskBasicInfo: info,
Partitions: []response.DiskBasicInfo{},
}
diskMap[info.Device] = disk
}
}
for parentDevice, partList := range partitions {
if disk, exists := diskMap[parentDevice]; exists {
for index, part := range partList {
part.Device = fmt.Sprintf("/dev/%s", part.Device)
if part.IsSystem {
disk.IsSystem = true
}
partList[index] = part
}
disk.Partitions = partList
}
}
var totalCapacity int64
for _, disk := range diskMap {
capacity := parseSizeToBytes(disk.Size)
totalCapacity += capacity
if disk.IsSystem {
systemDisk = disk
} else if len(disk.Partitions) == 0 {
result.UnpartitionedDisks = append(result.UnpartitionedDisks, disk.DiskBasicInfo)
} else {
result.Disks = append(result.Disks, *disk)
}
}
result.SystemDisk = systemDisk
result.TotalDisks = len(diskMap)
result.TotalCapacity = totalCapacity
return result
}
func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 {
return nil, fmt.Errorf("invalid lsblk output")
}
var diskInfos []response.DiskBasicInfo
lvmMap := make(map[string]response.DiskBasicInfo)
var pendingDevices []map[string]string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := parseKeyValuePairs(line)
name, ok := fields["NAME"]
if !ok {
continue
}
if strings.HasPrefix(name, "loop") ||
strings.HasPrefix(name, "dm-") {
continue
}
diskType := fields["TYPE"]
mountPoint := fields["MOUNTPOINT"]
fsType := fields["FSTYPE"]
size := fields["SIZE"]
if diskType == "lvm" {
total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name)
if total != "" {
size = total
}
lvmInfo := response.DiskBasicInfo{
Device: name,
Size: size,
Model: fields["MODEL"],
DiskType: "LVM",
IsRemovable: false,
IsSystem: isSystemDisk(mountPoint),
Filesystem: fsType,
Used: used,
Avail: avail,
UsePercent: usePercent,
MountPoint: mountPoint,
IsMounted: mountPoint != "" && mountPoint != "-",
Serial: fields["SERIAL"],
}
lvmMap[name] = lvmInfo
} else if diskType == "disk" || diskType == "part" {
pendingDevices = append(pendingDevices, fields)
}
}
for _, fields := range pendingDevices {
name := fields["NAME"]
size := fields["SIZE"]
mountPoint := fields["MOUNTPOINT"]
fsType := fields["FSTYPE"]
model := fields["MODEL"]
serial := fields["SERIAL"]
tran := fields["TRAN"]
rota := fields["ROTA"]
totalSize, used, avail, usePercent, _ := getDiskUsageInfo("/dev/" + name)
if totalSize != "" {
size = totalSize
}
actualMountPoint := mountPoint
actualFsType := fsType
actualUsed := used
actualAvail := avail
actualUsePercent := usePercent
isMounted := mountPoint != "" && mountPoint != "-"
isSystemPartition := isSystemDisk(mountPoint)
if fsType == "LVM2_member" {
for _, lvmInfo := range lvmMap {
if lvmInfo.IsMounted {
actualMountPoint = lvmInfo.MountPoint
actualFsType = lvmInfo.Filesystem
actualUsed = lvmInfo.Used
size = lvmInfo.Size
actualAvail = lvmInfo.Avail
actualUsePercent = lvmInfo.UsePercent
isMounted = true
isSystemPartition = lvmInfo.IsSystem
break
}
}
}
diskInfo := response.DiskBasicInfo{
Device: name,
Size: size,
Model: model,
DiskType: getDiskType(rota),
IsRemovable: tran == "usb",
IsSystem: isSystemPartition,
Filesystem: actualFsType,
Used: actualUsed,
Avail: actualAvail,
UsePercent: actualUsePercent,
MountPoint: actualMountPoint,
IsMounted: isMounted,
Serial: serial,
}
diskInfos = append(diskInfos, diskInfo)
}
return diskInfos, nil
}
func getDiskType(rota string) string {
if rota == "0" {
return "SSD"
} else if rota == "1" {
return "HDD"
}
return "Unknown"
}
var kvRe = regexp.MustCompile(`([A-Za-z0-9_]+)=("([^"\\]|\\.)*"|[^ \t]+)`)
func parseKeyValuePairs(line string) map[string]string {
fields := make(map[string]string)
matches := kvRe.FindAllStringSubmatch(line, -1)
for _, m := range matches {
key := m[1]
raw := m[2]
val := raw
if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' {
if unq, err := strconv.Unquote(val); err == nil {
val = unq
} else {
val = val[1 : len(val)-1]
}
}
fields[key] = val
}
return fields
}
func isPartitionDevice(device string) bool {
if strings.Contains(device, "nvme") {
return strings.Contains(device, "p") &&
strings.ContainsAny(device[strings.LastIndex(device, "p")+1:], "0123456789")
} else if strings.HasPrefix(device, "sd") || strings.HasPrefix(device, "hd") {
return len(device) > 3 &&
strings.ContainsAny(device[len(device)-1:], "0123456789")
} else if strings.HasPrefix(device, "vd") {
return len(device) > 3 &&
strings.ContainsAny(device[len(device)-1:], "0123456789")
}
return false
}
func getParentDevice(device string) string {
if strings.Contains(device, "nvme") {
if idx := strings.LastIndex(device, "p"); idx != -1 {
return device[:idx]
}
} else {
return strings.TrimRight(device, "0123456789")
}
return device
}
func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -h %s | tail -1", device))
if err != nil {
return "", "", "", 0, nil
}
fields := strings.Fields(output)
if len(fields) >= 5 {
size = fields[1]
used = fields[2]
avail = fields[3]
usePercentStr := strings.TrimSuffix(fields[4], "%")
usePercent, _ = strconv.Atoi(usePercentStr)
}
return size, used, avail, usePercent, 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 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 parseSizeToBytes(sizeStr string) int64 {
if sizeStr == "" || sizeStr == "-" {
return 0
}
sizeStr = strings.TrimSpace(sizeStr)
if len(sizeStr) < 2 {
return 0
}
unit := strings.ToUpper(sizeStr[len(sizeStr)-1:])
valueStr := sizeStr[:len(sizeStr)-1]
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
return 0
}
switch unit {
case "K":
return int64(value * 1024)
case "M":
return int64(value * 1024 * 1024)
case "G":
return int64(value * 1024 * 1024 * 1024)
case "T":
return int64(value * 1024 * 1024 * 1024 * 1024)
default:
val, _ := strconv.ParseInt(sizeStr, 10, 64)
return val
}
}
+1
View File
@@ -98,6 +98,7 @@ var WebUrlMap = map[string]struct{}{
"/hosts/ssh/ssh": {},
"/hosts/ssh/log": {},
"/hosts/ssh/session": {},
"/hosts/disk": {},
"/terminal": {},
@@ -30,7 +30,7 @@
<span class="ml-2">{{ diskInfo.model }}</span>
</el-text>
<el-text type="info" v-if="diskInfo.serial" class="flex items-center">
{{ $t('disk.model') }}:
{{ $t('disk.serial') }}:
<span class="ml-2">{{ diskInfo.serial }}</span>
</el-text>
<div v-if="scope == 'unpartitioned'">
@@ -118,10 +118,14 @@ const mount = (diskInfo: Host.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 () => {
ElMessageBox.confirm(
i18n.global.t('disk.unmountHelper', [diskInfo.device.split('/').pop()]),
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(() => {
@@ -2,7 +2,7 @@
<DrawerPro
v-model="open"
:header="$t('disk.' + operate)"
:resource="form.device"
:resource="form.device.split('/').pop()"
@close="handleClose"
v-loading="loading"
>
@@ -109,6 +109,7 @@ const submit = async () => {
const handleClose = () => {
open.value = false;
loading.value = false;
emit('search');
};