mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
feat: 添加磁盘管理工具到工具箱 (#1195)
* Initial plan * 实现磁盘管理工具的后端和前端基础功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * 完成磁盘管理工具功能实现并验证构建成功 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * 添加输入验证防止命令注入攻击 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * 移除命令注入验证并修复评审意见 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: merge main * feat: merge main * feat: 分区优化 * feat: fstab管理 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
76
internal/http/request/toolbox_disk.go
Normal file
76
internal/http/request/toolbox_disk.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package request
|
||||
|
||||
// ToolboxDiskDevice 磁盘设备请求
|
||||
type ToolboxDiskDevice struct {
|
||||
Device string `form:"device" json:"device" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxDiskMount 挂载请求
|
||||
type ToolboxDiskMount struct {
|
||||
Device string `form:"device" json:"device" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
WriteFstab bool `form:"write_fstab" json:"write_fstab"`
|
||||
MountOption string `form:"mount_option" json:"mount_option"`
|
||||
}
|
||||
|
||||
// ToolboxDiskUmount 卸载请求
|
||||
type ToolboxDiskUmount struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxDiskFormat 格式化请求
|
||||
type ToolboxDiskFormat struct {
|
||||
Device string `form:"device" json:"device" validate:"required"`
|
||||
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
|
||||
}
|
||||
|
||||
// ToolboxDiskVG 卷组请求
|
||||
type ToolboxDiskVG struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Devices []string `form:"devices" json:"devices" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxDiskLV 逻辑卷请求
|
||||
type ToolboxDiskLV struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
VGName string `form:"vg_name" json:"vg_name" validate:"required"`
|
||||
Size int `form:"size" json:"size" validate:"required|min:1"`
|
||||
}
|
||||
|
||||
// ToolboxDiskVGName 卷组名称请求
|
||||
type ToolboxDiskVGName struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxDiskLVPath 逻辑卷路径请求
|
||||
type ToolboxDiskLVPath struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxDiskExtendLV 扩容逻辑卷请求
|
||||
type ToolboxDiskExtendLV struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Size int `form:"size" json:"size" validate:"required|min:1"`
|
||||
Resize bool `form:"resize" json:"resize"`
|
||||
}
|
||||
|
||||
// ToolboxDiskInit 初始化磁盘请求
|
||||
type ToolboxDiskInit struct {
|
||||
Device string `form:"device" json:"device" validate:"required"`
|
||||
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
|
||||
}
|
||||
|
||||
// ToolboxDiskFstabEntry fstab 条目结构
|
||||
type ToolboxDiskFstabEntry struct {
|
||||
Device string `json:"device"` // 设备(UUID=xxx 或 /dev/xxx)
|
||||
MountPoint string `json:"mount_point"` // 挂载点
|
||||
FsType string `json:"fs_type"` // 文件系统类型
|
||||
Options string `json:"options"` // 挂载选项
|
||||
Dump string `json:"dump"` // 备份标志
|
||||
Pass string `json:"pass"` // 检查顺序
|
||||
}
|
||||
|
||||
// ToolboxDiskFstabDelete 删除 fstab 条目请求
|
||||
type ToolboxDiskFstabDelete struct {
|
||||
MountPoint string `form:"mount_point" json:"mount_point" validate:"required"`
|
||||
}
|
||||
@@ -49,6 +49,7 @@ type Http struct {
|
||||
toolboxSystem *service.ToolboxSystemService
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService
|
||||
toolboxSSH *service.ToolboxSSHService
|
||||
toolboxDisk *service.ToolboxDiskService
|
||||
webhook *service.WebHookService
|
||||
apps *apploader.Loader
|
||||
}
|
||||
@@ -87,6 +88,7 @@ func NewHttp(
|
||||
toolboxSystem *service.ToolboxSystemService,
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService,
|
||||
toolboxSSH *service.ToolboxSSHService,
|
||||
toolboxDisk *service.ToolboxDiskService,
|
||||
webhook *service.WebHookService,
|
||||
apps *apploader.Loader,
|
||||
) *Http {
|
||||
@@ -124,6 +126,7 @@ func NewHttp(
|
||||
toolboxSystem: toolboxSystem,
|
||||
toolboxBenchmark: toolboxBenchmark,
|
||||
toolboxSSH: toolboxSSH,
|
||||
toolboxDisk: toolboxDisk,
|
||||
webhook: webhook,
|
||||
apps: apps,
|
||||
}
|
||||
@@ -463,6 +466,25 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/root_key", route.toolboxSSH.GenerateRootKey)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_disk", func(r chi.Router) {
|
||||
r.Get("/list", route.toolboxDisk.List)
|
||||
r.Post("/partitions", route.toolboxDisk.GetPartitions)
|
||||
r.Post("/mount", route.toolboxDisk.Mount)
|
||||
r.Post("/umount", route.toolboxDisk.Umount)
|
||||
r.Post("/format", route.toolboxDisk.Format)
|
||||
r.Post("/init", route.toolboxDisk.Init)
|
||||
r.Get("/fstab", route.toolboxDisk.GetFstab)
|
||||
r.Delete("/fstab", route.toolboxDisk.DeleteFstab)
|
||||
r.Get("/lvm", route.toolboxDisk.GetLVMInfo)
|
||||
r.Post("/lvm/pv", route.toolboxDisk.CreatePV)
|
||||
r.Delete("/lvm/pv", route.toolboxDisk.RemovePV)
|
||||
r.Post("/lvm/vg", route.toolboxDisk.CreateVG)
|
||||
r.Delete("/lvm/vg", route.toolboxDisk.RemoveVG)
|
||||
r.Post("/lvm/lv", route.toolboxDisk.CreateLV)
|
||||
r.Delete("/lvm/lv", route.toolboxDisk.RemoveLV)
|
||||
r.Post("/lvm/lv/extend", route.toolboxDisk.ExtendLV)
|
||||
})
|
||||
|
||||
r.Route("/webhook", func(r chi.Router) {
|
||||
r.Get("/", route.webhook.List)
|
||||
r.Post("/", route.webhook.Create)
|
||||
|
||||
@@ -38,5 +38,6 @@ var ProviderSet = wire.NewSet(
|
||||
NewToolboxSystemService,
|
||||
NewToolboxBenchmarkService,
|
||||
NewToolboxSSHService,
|
||||
NewToolboxDiskService,
|
||||
NewWsService,
|
||||
)
|
||||
|
||||
567
internal/service/toolbox_disk.go
Normal file
567
internal/service/toolbox_disk.go
Normal file
@@ -0,0 +1,567 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type ToolboxDiskService struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewToolboxDiskService(t *gotext.Locale) *ToolboxDiskService {
|
||||
return &ToolboxDiskService{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取磁盘列表
|
||||
func (s *ToolboxDiskService) List(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取磁盘基本信息
|
||||
lsblkOutput, err := shell.Execf("lsblk -J -b -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,UUID,LABEL,MODEL")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get disk list: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 lsblk JSON
|
||||
var lsblkData struct {
|
||||
BlockDevices []any `json:"blockdevices"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(lsblkOutput), &lsblkData); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to parse disk list: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取磁盘使用情况
|
||||
dfOutput, _ := shell.Execf("df -B1 --output=source,size,used,avail,pcent,target 2>/dev/null | tail -n +2")
|
||||
|
||||
// 解析 df 输出为 map
|
||||
dfMap := make(map[string]map[string]string)
|
||||
lines := strings.Split(strings.TrimSpace(dfOutput), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 6 {
|
||||
mountpoint := fields[5]
|
||||
dfMap[mountpoint] = map[string]string{
|
||||
"size": fields[1],
|
||||
"used": fields[2],
|
||||
"avail": fields[3],
|
||||
"percent": strings.TrimSuffix(fields[4], "%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"disks": lsblkData.BlockDevices,
|
||||
"df": dfMap,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPartitions 获取分区列表
|
||||
func (s *ToolboxDiskService) GetPartitions(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskDevice](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := shell.Execf("lsblk -J -b -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,UUID,LABEL '/dev/%s'", req.Device)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get partitions: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, output)
|
||||
}
|
||||
|
||||
// Mount 挂载分区
|
||||
func (s *ToolboxDiskService) Mount(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskMount](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("test -d '%s' || mkdir -p '%s'", req.Path, req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create mount point: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("mount '/dev/%s' '%s'", req.Device, req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to mount partition: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果需要写入 fstab
|
||||
if req.WriteFstab {
|
||||
// 获取分区的 UUID
|
||||
uuid, err := shell.Execf("blkid -s UUID -o value '/dev/%s'", req.Device)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get partition UUID: %v", err))
|
||||
return
|
||||
}
|
||||
uuid = strings.TrimSpace(uuid)
|
||||
if uuid == "" {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("partition has no UUID"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件系统类型
|
||||
fsType, err := shell.Execf("blkid -s TYPE -o value '/dev/%s'", req.Device)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get filesystem type: %v", err))
|
||||
return
|
||||
}
|
||||
fsType = strings.TrimSpace(fsType)
|
||||
if fsType == "" {
|
||||
fsType = "auto"
|
||||
}
|
||||
|
||||
// 挂载选项
|
||||
mountOption := req.MountOption
|
||||
if mountOption == "" {
|
||||
mountOption = "defaults"
|
||||
}
|
||||
|
||||
// 检查 fstab 中是否已存在该挂载点
|
||||
existCheck, _ := shell.Execf("grep -E '^[^#].*\\s+%s\\s+' /etc/fstab", req.Path)
|
||||
if strings.TrimSpace(existCheck) != "" {
|
||||
Error(w, http.StatusBadRequest, s.t.Get("mount point %s already exists in fstab", req.Path))
|
||||
return
|
||||
}
|
||||
|
||||
// 写入 fstab
|
||||
fstabEntry := fmt.Sprintf("UUID=%s %s %s %s 0 2", uuid, req.Path, fsType, mountOption)
|
||||
if _, err = shell.Execf("echo '%s' >> /etc/fstab", fstabEntry); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to write fstab: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Umount 卸载分区
|
||||
func (s *ToolboxDiskService) Umount(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskUmount](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("umount '%s'", req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to umount partition: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Format 格式化分区
|
||||
func (s *ToolboxDiskService) Format(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskFormat](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var formatCmd string
|
||||
switch req.FsType {
|
||||
case "ext4":
|
||||
formatCmd = fmt.Sprintf("mkfs.ext4 -F '/dev/%s'", req.Device)
|
||||
case "ext3":
|
||||
formatCmd = fmt.Sprintf("mkfs.ext3 -F '/dev/%s'", req.Device)
|
||||
case "xfs":
|
||||
formatCmd = fmt.Sprintf("mkfs.xfs -f '/dev/%s'", req.Device)
|
||||
case "btrfs":
|
||||
formatCmd = fmt.Sprintf("mkfs.btrfs -f '/dev/%s'", req.Device)
|
||||
default:
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("unsupported filesystem type: %s", req.FsType))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(formatCmd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to format partition: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Init 初始化磁盘(删除所有分区,创建单个分区并格式化)
|
||||
func (s *ToolboxDiskService) Init(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskInit](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
device := "/dev/" + req.Device
|
||||
|
||||
// 检查设备是否存在
|
||||
if _, err = shell.Execf("test -b '%s'", device); err != nil {
|
||||
Error(w, http.StatusBadRequest, s.t.Get("device not found: %s", device))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否为系统盘(检查是否有分区挂载在 /)
|
||||
mountInfo, _ := shell.Execf("lsblk -no MOUNTPOINT '%s' 2>/dev/null", device)
|
||||
if strings.Contains(mountInfo, "/\n") || strings.TrimSpace(mountInfo) == "/" {
|
||||
Error(w, http.StatusBadRequest, s.t.Get("cannot initialize system disk"))
|
||||
return
|
||||
}
|
||||
|
||||
// 卸载该磁盘的所有分区
|
||||
_, _ = shell.Execf("umount '%s'* 2>/dev/null || true", device)
|
||||
|
||||
// 先清除分区表
|
||||
if _, err = shell.Execf("wipefs -a '%s'", device); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to wipe disk: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// sfdisk 创建 GPT 分区表和单个分区
|
||||
if _, err = shell.Execf("echo 'type=linux' | sfdisk '%s'", device); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create partition: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 等待内核更新分区表
|
||||
_, _ = shell.Execf("partprobe '%s' 2>/dev/null || true", device)
|
||||
_, _ = shell.Execf("sleep 1")
|
||||
|
||||
// 确定新分区的设备名(device + "1",如 sdb1 或 nvme0n1p1)
|
||||
var partDevice string
|
||||
if strings.Contains(req.Device, "nvme") || strings.Contains(req.Device, "loop") {
|
||||
partDevice = device + "p1"
|
||||
} else {
|
||||
partDevice = device + "1"
|
||||
}
|
||||
|
||||
// 格式化新分区
|
||||
var formatCmd string
|
||||
switch req.FsType {
|
||||
case "ext4":
|
||||
formatCmd = fmt.Sprintf("mkfs.ext4 -F '%s'", partDevice)
|
||||
case "ext3":
|
||||
formatCmd = fmt.Sprintf("mkfs.ext3 -F '%s'", partDevice)
|
||||
case "xfs":
|
||||
formatCmd = fmt.Sprintf("mkfs.xfs -f '%s'", partDevice)
|
||||
case "btrfs":
|
||||
formatCmd = fmt.Sprintf("mkfs.btrfs -f '%s'", partDevice)
|
||||
default:
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("unsupported filesystem type: %s", req.FsType))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(formatCmd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to format partition: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetLVMInfo 获取LVM信息
|
||||
func (s *ToolboxDiskService) GetLVMInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取物理卷信息
|
||||
pvOutput, _ := shell.Execf("pvdisplay -C --noheadings --separator '|' -o pv_name,vg_name,pv_size,pv_free 2>/dev/null || echo ''")
|
||||
// 获取卷组信息
|
||||
vgOutput, _ := shell.Execf("vgdisplay -C --noheadings --separator '|' -o vg_name,pv_count,lv_count,vg_size,vg_free 2>/dev/null || echo ''")
|
||||
// 获取逻辑卷信息
|
||||
lvOutput, _ := shell.Execf("lvdisplay -C --noheadings --separator '|' -o lv_name,vg_name,lv_size,lv_path 2>/dev/null || echo ''")
|
||||
|
||||
pvs := s.parseLVMOutput(pvOutput)
|
||||
vgs := s.parseLVMOutput(vgOutput)
|
||||
lvs := s.parseLVMOutput(lvOutput)
|
||||
|
||||
Success(w, chix.M{
|
||||
"pvs": pvs,
|
||||
"vgs": vgs,
|
||||
"lvs": lvs,
|
||||
})
|
||||
}
|
||||
|
||||
// CreatePV 创建物理卷
|
||||
func (s *ToolboxDiskService) CreatePV(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskDevice](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("pvcreate '/dev/%s'", req.Device); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create physical volume: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// CreateVG 创建卷组
|
||||
func (s *ToolboxDiskService) CreateVG(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskVG](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建设备列表,每个设备单独引用
|
||||
var deviceArgs []string
|
||||
for _, dev := range req.Devices {
|
||||
deviceArgs = append(deviceArgs, fmt.Sprintf("'%s'", dev))
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("vgcreate '%s' %s", req.Name, strings.Join(deviceArgs, " ")); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create volume group: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// CreateLV 创建逻辑卷
|
||||
func (s *ToolboxDiskService) CreateLV(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskLV](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证逻辑卷大小(必须为正数)
|
||||
if req.Size <= 0 {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid logical volume size"))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建逻辑卷
|
||||
if _, err = shell.Execf("lvcreate -L '%dG' -n '%s' '%s'", req.Size, req.Name, req.VGName); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create logical volume: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// RemovePV 删除物理卷
|
||||
func (s *ToolboxDiskService) RemovePV(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskDevice](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("pvremove '%s'", req.Device); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove physical volume: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// RemoveVG 删除卷组
|
||||
func (s *ToolboxDiskService) RemoveVG(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskVGName](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("vgremove -f '%s'", req.Name); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove volume group: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// RemoveLV 删除逻辑卷
|
||||
func (s *ToolboxDiskService) RemoveLV(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskLVPath](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("lvremove -f '%s'", req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove logical volume: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// ExtendLV 扩容逻辑卷
|
||||
func (s *ToolboxDiskService) ExtendLV(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskExtendLV](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证扩容大小为正整数
|
||||
if req.Size <= 0 {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid size"))
|
||||
return
|
||||
}
|
||||
|
||||
// 扩容逻辑卷
|
||||
if _, err = shell.Execf("lvextend -L +%dG '%s'", req.Size, req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to extend logical volume: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 扩展文件系统
|
||||
if req.Resize {
|
||||
// 检测文件系统类型并扩展
|
||||
fsType, _ := shell.Execf("blkid -o value -s TYPE '%s'", req.Path)
|
||||
fsType = strings.TrimSpace(fsType)
|
||||
|
||||
switch fsType {
|
||||
case "ext4", "ext3":
|
||||
if _, err = shell.Execf("resize2fs '%s'", req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
|
||||
return
|
||||
}
|
||||
case "xfs":
|
||||
// XFS需要挂载后才能扩展
|
||||
mountPoint, _ := shell.Execf("findmnt -n -o TARGET '%s'", req.Path)
|
||||
mountPoint = strings.TrimSpace(mountPoint)
|
||||
if mountPoint != "" {
|
||||
if _, err = shell.Execf("xfs_growfs '%s'", mountPoint); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// XFS未挂载时,返回错误信息
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("xfs filesystem is not mounted, logical volume has been extended but filesystem was not resized"))
|
||||
return
|
||||
}
|
||||
case "btrfs":
|
||||
// btrfs需要挂载后才能扩展
|
||||
mountPoint, _ := shell.Execf("findmnt -n -o TARGET '%s'", req.Path)
|
||||
mountPoint = strings.TrimSpace(mountPoint)
|
||||
if mountPoint != "" {
|
||||
// 扩展到当前可用的最大空间
|
||||
if _, err = shell.Execf("btrfs filesystem resize max '%s'", mountPoint); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// btrfs未挂载时,返回错误信息
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("btrfs filesystem is not mounted, logical volume has been extended but filesystem was not resized"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// parseLVMOutput 解析LVM命令输出
|
||||
// 将LVM命令的表格输出解析为map数组,每行数据的字段以field_0, field_1...命名
|
||||
var spaceRegex = regexp.MustCompile(`\s+`)
|
||||
|
||||
func (s *ToolboxDiskService) parseLVMOutput(output string) []map[string]string {
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
var result []map[string]string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
line = spaceRegex.ReplaceAllString(line, " ")
|
||||
|
||||
fields := strings.Split(line, "|")
|
||||
item := make(map[string]string)
|
||||
|
||||
for i, field := range fields {
|
||||
item[fmt.Sprintf("field_%d", i)] = strings.TrimSpace(field)
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetFstab 获取 fstab 列表
|
||||
func (s *ToolboxDiskService) GetFstab(w http.ResponseWriter, r *http.Request) {
|
||||
content, err := shell.Execf("cat /etc/fstab")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to read fstab: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var entries []request.ToolboxDiskFstabEntry
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// 跳过空行和注释
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 4 {
|
||||
entry := request.ToolboxDiskFstabEntry{
|
||||
Device: fields[0],
|
||||
MountPoint: fields[1],
|
||||
FsType: fields[2],
|
||||
Options: fields[3],
|
||||
}
|
||||
if len(fields) >= 5 {
|
||||
entry.Dump = fields[4]
|
||||
}
|
||||
if len(fields) >= 6 {
|
||||
entry.Pass = fields[5]
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, entries)
|
||||
}
|
||||
|
||||
// DeleteFstab 删除 fstab 条目
|
||||
func (s *ToolboxDiskService) DeleteFstab(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxDiskFstabDelete](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 不允许删除根目录挂载
|
||||
if req.MountPoint == "/" {
|
||||
Error(w, http.StatusBadRequest, s.t.Get("cannot delete root mount point"))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`sed -i 's@^[^#].*\s%s\s.*$@@g' /etc/fstab`, req.MountPoint); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to delete fstab entry: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("mount -a"); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to remount filesystems: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user