mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -119,6 +119,7 @@ func initWeb() (*app.Web, error) {
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
@@ -142,7 +143,7 @@ func initWeb() (*app.Web, error) {
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, webHookService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, webHookService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
46
web/src/api/panel/toolbox-disk/index.ts
Normal file
46
web/src/api/panel/toolbox-disk/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取磁盘列表
|
||||
list: (): any => http.Get('/toolbox_disk/list'),
|
||||
// 获取分区列表
|
||||
partitions: (device: string): any => http.Post('/toolbox_disk/partitions', { device }),
|
||||
// 挂载分区
|
||||
mount: (
|
||||
device: string,
|
||||
path: string,
|
||||
write_fstab: boolean = false,
|
||||
mount_option: string = ''
|
||||
): any => http.Post('/toolbox_disk/mount', { device, path, write_fstab, mount_option }),
|
||||
// 卸载分区
|
||||
umount: (path: string): any => http.Post('/toolbox_disk/umount', { path }),
|
||||
// 格式化分区
|
||||
format: (device: string, fs_type: string): any =>
|
||||
http.Post('/toolbox_disk/format', { device, fs_type }),
|
||||
// 初始化磁盘
|
||||
init: (device: string, fs_type: string): any =>
|
||||
http.Post('/toolbox_disk/init', { device, fs_type }),
|
||||
// 获取 fstab 列表
|
||||
fstabList: (): any => http.Get('/toolbox_disk/fstab'),
|
||||
// 删除 fstab 条目
|
||||
fstabDelete: (mount_point: string): any => http.Delete('/toolbox_disk/fstab', { mount_point }),
|
||||
// 获取LVM信息
|
||||
lvmInfo: (): any => http.Get('/toolbox_disk/lvm'),
|
||||
// 创建物理卷
|
||||
createPV: (device: string): any => http.Post('/toolbox_disk/lvm/pv', { device }),
|
||||
// 删除物理卷
|
||||
removePV: (device: string): any => http.Delete('/toolbox_disk/lvm/pv', { device }),
|
||||
// 创建卷组
|
||||
createVG: (name: string, devices: string[]): any =>
|
||||
http.Post('/toolbox_disk/lvm/vg', { name, devices }),
|
||||
// 删除卷组
|
||||
removeVG: (name: string): any => http.Delete('/toolbox_disk/lvm/vg', { name }),
|
||||
// 创建逻辑卷
|
||||
createLV: (name: string, vg_name: string, size: number): any =>
|
||||
http.Post('/toolbox_disk/lvm/lv', { name, vg_name, size }),
|
||||
// 删除逻辑卷
|
||||
removeLV: (path: string): any => http.Delete('/toolbox_disk/lvm/lv', { path }),
|
||||
// 扩容逻辑卷
|
||||
extendLV: (path: string, size: number, resize: boolean): any =>
|
||||
http.Post('/toolbox_disk/lvm/lv/extend', { path, size, resize })
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1020
web/src/views/toolbox/DiskView.vue
Normal file
1020
web/src/views/toolbox/DiskView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,9 @@ defineOptions({
|
||||
})
|
||||
|
||||
import BenchmarkView from '@/views/toolbox/BenchmarkView.vue'
|
||||
import DiskView from '@/views/toolbox/DiskView.vue'
|
||||
import ProcessView from '@/views/toolbox/ProcessView.vue'
|
||||
import SSHView from '@/views/toolbox/SSHView.vue'
|
||||
import SshView from '@/views/toolbox/SshView.vue'
|
||||
import SystemView from '@/views/toolbox/SystemView.vue'
|
||||
import WebHookView from '@/views/toolbox/WebHookView.vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
@@ -21,6 +22,7 @@ const current = ref('process')
|
||||
<n-tab name="process" :tab="$gettext('Process')" />
|
||||
<n-tab name="system" :tab="$gettext('System')" />
|
||||
<n-tab name="ssh" tab="SSH" />
|
||||
<n-tab name="disk" :tab="$gettext('Disk')" />
|
||||
<n-tab name="webhook" :tab="$gettext('WebHook')" />
|
||||
<n-tab name="benchmark" :tab="$gettext('Benchmark')" />
|
||||
</n-tabs>
|
||||
@@ -28,7 +30,8 @@ const current = ref('process')
|
||||
<n-flex vertical>
|
||||
<process-view v-if="current === 'process'" />
|
||||
<system-view v-if="current === 'system'" />
|
||||
<s-s-h-view v-if="current === 'ssh'" />
|
||||
<ssh-view v-if="current === 'ssh'" />
|
||||
<disk-view v-if="current === 'disk'" />
|
||||
<web-hook-view v-if="current === 'webhook'" />
|
||||
<benchmark-view v-if="current === 'benchmark'" />
|
||||
</n-flex>
|
||||
|
||||
@@ -224,20 +224,16 @@ onMounted(() => {
|
||||
<template>
|
||||
<n-spin :show="loading">
|
||||
<n-flex vertical :size="24">
|
||||
<!-- SSH 服务状态 -->
|
||||
<!-- SSH 服务 -->
|
||||
<n-card :title="$gettext('SSH Service')">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Service Status') }}</n-text>
|
||||
<n-switch :value="sshStatus" :loading="loading" @update:value="handleToggleSSH" />
|
||||
<n-button :loading="loading" @click="handleRestartSSH">
|
||||
{{ $gettext('Restart') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- SSH 基础设置 -->
|
||||
<n-card :title="$gettext('SSH Basic Settings')">
|
||||
<n-flex vertical :size="16">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Service Status') }}</n-text>
|
||||
<n-switch :value="sshStatus" :loading="loading" @update:value="handleToggleSSH" />
|
||||
<n-button :loading="loading" @click="handleRestartSSH">
|
||||
{{ $gettext('Restart') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<!-- SSH 密码登录 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
@@ -250,7 +246,6 @@ onMounted(() => {
|
||||
</n-flex>
|
||||
<n-text depth="3">{{ $gettext('Allow password authentication for SSH login') }}</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- SSH 密钥登录 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
@@ -265,7 +260,6 @@ onMounted(() => {
|
||||
$gettext('Allow key authentication for SSH login')
|
||||
}}</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- SSH 端口 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
@@ -299,7 +293,6 @@ onMounted(() => {
|
||||
@update:value="handleUpdateRootLogin"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
<!-- Root 密码 -->
|
||||
<n-flex vertical :size="8">
|
||||
<n-text strong>{{ $gettext('Root Password') }}</n-text>
|
||||
@@ -332,7 +325,6 @@ onMounted(() => {
|
||||
}}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- Root 密钥 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
@@ -290,7 +290,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-flex justify="end">
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="createModal = true">
|
||||
{{ $gettext('Create WebHook') }}
|
||||
</n-button>
|
||||
|
||||
Reference in New Issue
Block a user