1Panel/agent/app/service/disk_utils.go

570 lines
13 KiB
Go

package service
import (
"bufio"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"os"
"os/exec"
"strconv"
"strings"
"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"
)
type LsblkRaw struct {
Blockdevices []LsblkDevice `json:"blockdevices"`
}
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"`
}
func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
var list []response.DiskBasicInfo
if strings.HasPrefix(dev.Name, "loop") || strings.HasPrefix(dev.Name, "dm-") || dev.Type == "rom" {
return list
}
if dev.Type == "lvm" {
return list
}
diskType := "Unknown"
if dev.Type == "disk" || dev.Type == "part" {
if dev.Rota {
diskType = "HDD"
} else {
diskType = "SSD"
}
}
mountPoint := dev.Mountpoint
filesystem := dev.Fstype
size := dev.Size
var used, avail, totalSize string
var usePercent int
isMounted := mountPoint != ""
isSystem := false
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
for _, child := range dev.Children {
if child.Type == "lvm" && child.Mountpoint != "" {
devicePath := "/dev/mapper/" + child.Name
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(devicePath)
childInfo := response.DiskBasicInfo{
Device: dev.Name,
Size: totalSize,
Model: dev.Model,
DiskType: diskType,
Filesystem: child.Fstype,
MountPoint: child.Mountpoint,
IsMounted: true,
UsePercent: usePercent,
Used: used,
Avail: avail,
Serial: dev.Serial,
IsRemovable: dev.Tran == "usb",
IsSystem: isSystemDisk(child.Mountpoint),
}
list = append(list, childInfo)
}
}
return list
} else if isMounted {
isSystem = isSystemDisk(mountPoint)
devicePath := "/dev/" + dev.Name
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(devicePath)
if totalSize != "" {
size = totalSize
}
}
info := response.DiskBasicInfo{
Device: dev.Name,
Size: size,
Model: dev.Model,
DiskType: diskType,
Filesystem: filesystem,
MountPoint: mountPoint,
IsMounted: isMounted,
UsePercent: usePercent,
Used: used,
Avail: avail,
Serial: dev.Serial,
IsRemovable: dev.Tran == "usb",
IsSystem: isSystem,
}
list = append(list, info)
for _, child := range dev.Children {
childList := parseDevice(child)
list = append(list, childList...)
}
return list
}
func parseLsblkJsonOutput(output string) ([]response.DiskBasicInfo, error) {
raw := &LsblkRaw{}
if err := json.Unmarshal([]byte(output), raw); err != nil {
return nil, fmt.Errorf("failed to parse lsblk json output: %v", err)
}
var disks []response.DiskBasicInfo
for _, dev := range raw.Blockdevices {
if strings.HasPrefix(dev.Name, "loop") ||
strings.HasPrefix(dev.Name, "dm-") {
continue
}
devList := parseDevice(dev)
disks = append(disks, devList...)
}
return disks, nil
}
func organizeDiskInfo(diskInfos []response.DiskBasicInfo) response.CompleteDiskInfo {
var result response.CompleteDiskInfo
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 {
result.SystemDisks = append(result.SystemDisks, *disk)
} else if len(disk.Partitions) == 0 {
result.UnpartitionedDisks = append(result.UnpartitionedDisks, disk.DiskBasicInfo)
} else {
result.Disks = append(result.Disks, *disk)
}
}
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 != "" && fsType != "" {
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"]
var (
used, avail, totalSize string
usePercent int
)
if mountPoint != "" {
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 {
lvmDiskInfo := response.DiskBasicInfo{
Device: name,
Size: lvmInfo.Size,
Model: model,
DiskType: "LVM",
IsRemovable: tran == "usb",
IsSystem: lvmInfo.IsSystem,
Filesystem: lvmInfo.Filesystem,
Used: lvmInfo.Used,
Avail: lvmInfo.Avail,
UsePercent: lvmInfo.UsePercent,
MountPoint: lvmInfo.MountPoint,
IsMounted: true,
Serial: serial,
}
diskInfos = append(diskInfos, lvmDiskInfo)
}
}
continue
}
info := 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, info)
}
return diskInfos, nil
}
func getDiskType(rota string) string {
if rota == "0" {
return "SSD"
} else if rota == "1" {
return "HDD"
}
return "Unknown"
}
func parseKeyValuePairs(line string) map[string]string {
fields := make(map[string]string)
matches := re.GetRegex(re.DiskKeyValuePattern).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",
"/etc",
}
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
}
}