diff --git a/.gitignore b/.gitignore index b04fa7782..223d1a098 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ opencode.json superpowers .worktrees/ .codex/ + +/agent/utils/vm/helper/bin/1panel-kvm* diff --git a/Makefile b/Makefile index ee29f2467..f85c2de17 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,10 @@ build_core_on_linux: cd $(CORE_PATH) \ && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN) -build_agent_on_linux: +prepare_vm_helper: + bash $(AGENT_PATH)/utils/vm/helper/prepare.sh + +build_agent_on_linux: prepare_vm_helper cd $(AGENT_PATH) \ && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN) @@ -40,7 +43,7 @@ build_core_on_darwin: cd $(CORE_PATH) \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN) -build_agent_on_darwin: +build_agent_on_darwin: prepare_vm_helper cd $(AGENT_PATH) \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN) diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index 37f4f251e..da3a47f00 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -11,6 +11,7 @@ var ApiGroupApp = new(ApiGroup) type BaseApi struct{} var ( + vmService = service.NewIVMService() dashboardService = service.NewIDashboardService() appService = service.NewIAppService() diff --git a/agent/app/api/v2/vm.go b/agent/app/api/v2/vm.go new file mode 100644 index 000000000..8f3999fd2 --- /dev/null +++ b/agent/app/api/v2/vm.go @@ -0,0 +1,1001 @@ +package v2 + +import ( + "errors" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "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/re" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +// @Tags VirtualMachine +// @Summary Check virtual machine environment +// @Accept json +// @Produce json +// @Success 200 {object} dto.VMEnvironment +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/environment [get] +func (b *BaseApi) CheckVMEnvironment(c *gin.Context) { + data, err := vmService.CheckEnvironment() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Enable virtual machine environment +// @Accept json +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/environment/enable [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"启用虚拟机运行环境","formatEN":"enable VM runtime environment"} +func (b *BaseApi) EnableVMEnvironment(c *gin.Context) { + if err := vmService.EnableEnvironment(); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Page virtual machines +// @Description Returns the VM list and the target node quota: 1 for Community, 5 for Professional, and 0 for unlimited Enterprise. +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.VMSearchResult{items=[]dto.VMInfo} +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/search [post] +func (b *BaseApi) SearchVM(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := vmService.Search(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, dto.VMSearchResult{ + PageResult: dto.PageResult{Items: list, Total: total}, + Limit: xpack.VMCountLimit(c), + }) +} + +// @Tags VirtualMachine +// @Summary Sync virtual machines +// @Accept json +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步虚拟机","formatEN":"sync virtual machines"} +func (b *BaseApi) SyncVMs(c *gin.Context) { + if err := vmService.SyncVMs(); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Load virtual machine details +// @Accept json +// @Param request body dto.VMInfoReq true "request" +// @Produce json +// @Success 200 {object} dto.VMInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/info [post] +func (b *BaseApi) GetVM(c *gin.Context) { + var req dto.VMInfoReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Get(req.Name, req.CheckDisks) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Expand virtual machine disk +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMDiskResize true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/disks/resize [post] +// @x-panel-log {"bodyKeys":["name","target","sizeGiB"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB","formatEN":"expand VM [name] disk [target] to [sizeGiB] GiB"} +func (b *BaseApi) ResizeVMDisk(c *gin.Context) { + var req dto.VMDiskResize + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.ResizeDisk(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Check virtual machine health +// @Accept json +// @Param request body dto.VMReq true "request" +// @Produce json +// @Success 200 {object} dto.VMHealthCheckResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/health/check [post] +func (b *BaseApi) HealthCheckVM(c *gin.Context) { + var req dto.VMReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.HealthCheck(c.Request.Context(), req.Name) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Load virtual machine statistics +// @Accept json +// @Param request body dto.VMReq true "request" +// @Produce json +// @Success 200 {object} dto.VMStats +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/stats [post] +func (b *BaseApi) GetVMStats(c *gin.Context) { + var req dto.VMReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.GetStats(req.Name) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine +// @Description Creates a VM from ISO images or a template. The target node allows 1 VM in Community, 5 in Professional, and unlimited VMs in Enterprise. All VM records count toward the quota, regardless of state; creation fails with ErrVMCountLimit when the quota is reached. +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMCreate true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机 [name]","formatEN":"create VM [name]"} +func (b *BaseApi) CreateVM(c *gin.Context) { + var req dto.VMCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Create(req, xpack.VMCountLimit(c)) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary List virtual machine templates +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMTemplateInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/templates/list [post] +func (b *BaseApi) ListVMTemplates(c *gin.Context) { + data, err := vmService.ListTemplates() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine template +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMTemplateCreate true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/templates [post] +// @x-panel-log {"bodyKeys":["name","templateName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机 [name] 模板 [templateName]","formatEN":"create VM [name] template [templateName]"} +func (b *BaseApi) CreateVMTemplate(c *gin.Context) { + var req dto.VMTemplateCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.CreateTemplate(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine template +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMTemplateDelete true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/templates/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_templates","output_column":"name","output_value":"name"}],"formatZH":"删除虚拟机模板 [name]","formatEN":"delete VM template [name]"} +func (b *BaseApi) DeleteVMTemplate(c *gin.Context) { + var req dto.VMTemplateDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.DeleteTemplate(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Update virtual machine +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMUpdate true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新虚拟机 [name]","formatEN":"update VM [name]"} +func (b *BaseApi) UpdateVM(c *gin.Context) { + var req dto.VMUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Update(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Rename virtual machine +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMRename true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/rename [post] +// @x-panel-log {"bodyKeys":["name","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重命名虚拟机 [name] 为 [newName]","formatEN":"rename VM [name] to [newName]"} +func (b *BaseApi) RenameVM(c *gin.Context) { + var req dto.VMRename + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Rename(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMDelete true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"virtual_machines","output_column":"name","output_value":"name"}],"formatZH":"删除虚拟机 [name]","formatEN":"delete VM [name]"} +func (b *BaseApi) DeleteVM(c *gin.Context) { + var req dto.VMDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Delete(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary List virtual machine snapshots +// @Accept json +// @Param request body dto.VMSnapshotSearch true "request" +// @Produce json +// @Success 200 {array} dto.VMSnapshotInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/snapshots/list [post] +func (b *BaseApi) ListVMSnapshots(c *gin.Context) { + var req dto.VMSnapshotSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.ListSnapshots(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine snapshot +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMSnapshotReq true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/snapshots [post] +// @x-panel-log {"bodyKeys":["name","snapshotName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机 [name] 快照 [snapshotName]","formatEN":"create VM [name] snapshot [snapshotName]"} +func (b *BaseApi) CreateVMSnapshot(c *gin.Context) { + var req dto.VMSnapshotReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.CreateSnapshot(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Recover virtual machine snapshot +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMSnapshotReq true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/snapshots/recover [post] +// @x-panel-log {"bodyKeys":["name","snapshotName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"恢复虚拟机 [name] 快照 [snapshotName]","formatEN":"recover VM [name] snapshot [snapshotName]"} +func (b *BaseApi) RecoverVMSnapshot(c *gin.Context) { + var req dto.VMSnapshotReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.RecoverSnapshot(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine snapshot +// @Description Returns taskID for tracking the asynchronous operation. +// @Accept json +// @Param request body dto.VMSnapshotReq true "request" +// @Produce json +// @Success 200 {object} dto.VMTaskResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/snapshots/del [post] +// @x-panel-log {"bodyKeys":["name","snapshotName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除虚拟机 [name] 快照 [snapshotName]","formatEN":"delete VM [name] snapshot [snapshotName]"} +func (b *BaseApi) DeleteVMSnapshot(c *gin.Context) { + var req dto.VMSnapshotReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.DeleteSnapshot(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Scan orphaned virtual machine data +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMOrphanItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/orphans/scan [post] +func (b *BaseApi) ScanVMOrphans(c *gin.Context) { + data, err := vmService.ScanOrphans() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Clean orphaned virtual machine data +// @Accept json +// @Param request body dto.VMOrphanClean true "request" +// @Produce json +// @Success 200 {array} dto.VMOrphanItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/orphans/clean [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理虚拟机孤立数据","formatEN":"clean VM orphaned data"} +func (b *BaseApi) CleanVMOrphans(c *gin.Context) { + var req dto.VMOrphanClean + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.CleanOrphans(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Operate virtual machine +// @Accept json +// @Param request body dto.VMOperate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/operate [post] +// @x-panel-log {"bodyKeys":["name","operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作虚拟机 [name] [operate]","formatEN":"operate VM [name] [operate]"} +func (b *BaseApi) OperateVM(c *gin.Context) { + var req dto.VMOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.Operate(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Load virtual machine console information +// @Accept json +// @Param request body dto.VMReq true "request" +// @Produce json +// @Success 200 {object} dto.VMGraphics +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/console [post] +func (b *BaseApi) GetVMConsole(c *gin.Context) { + var req dto.VMReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := vmService.Console(req.Name) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Connect virtual machine VNC console over WebSocket +// @Description Upgrades the connection to WebSocket and forwards VNC traffic. +// @Param name query string true "virtual machine name" +// @Success 101 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/console/ws [get] +func (b *BaseApi) ProxyVMConsole(c *gin.Context) { + if !websocket.IsWebSocketUpgrade(c.Request) { + helper.BadRequest(c, errors.New("websocket upgrade required")) + return + } + itemName := c.Query("name") + if !validateVMName(c, itemName) { + return + } + graphics, err := vmService.Console(itemName) + if err != nil { + helper.InternalServer(c, err) + return + } + addr, err := buildVNCAddress(graphics) + if err != nil { + helper.BadRequest(c, err) + return + } + + wsConn, err := vmConsoleUpgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("upgrade vm console websocket failed, err: %v", err) + return + } + defer wsConn.Close() + + vncConn, err := net.DialTimeout("tcp", addr, 10*time.Second) + if err != nil { + global.LOG.Errorf("connect vm vnc failed, vm=%s, addr=%s, err=%v", itemName, addr, err) + writeVMConsoleClose(wsConn, fmt.Sprintf("connect vnc failed: %v", err)) + return + } + defer vncConn.Close() + + errCh := make(chan error, 2) + go forwardVNCToWebSocket(wsConn, vncConn, errCh) + go forwardWebSocketToVNC(wsConn, vncConn, errCh) + if err := <-errCh; err != nil && !errors.Is(err, io.EOF) { + global.LOG.Debugf("vm console disconnected, vm=%s, err=%v", itemName, err) + } +} + +// @Tags VirtualMachine +// @Summary Page virtual machine ISO images +// @Accept json +// @Param request body dto.SearchPageWithType true "request" +// @Produce json +// @Success 200 {object} dto.PageResult{items=[]dto.VMIsoInfo} +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/search [post] +func (b *BaseApi) SearchVMIso(c *gin.Context) { + var req dto.SearchPageWithType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := vmService.SearchIso(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{Items: list, Total: total}) +} + +// @Tags VirtualMachine +// @Summary List virtual machine ISO images +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMIsoInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/list [post] +func (b *BaseApi) ListVMIso(c *gin.Context) { + data, err := vmService.ListIso() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine ISO image record +// @Accept json +// @Param request body dto.VMIsoCreate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机镜像 [name] [type]","formatEN":"create VM ISO [name] [type]"} +func (b *BaseApi) CreateVMIso(c *gin.Context) { + var req dto.VMIsoCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.CreateIso(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Start virtual machine ISO image import +// @Description Creates an import record and returns its id. +// @Accept json +// @Param request body dto.VMIsoImportStart true "request" +// @Produce json +// @Success 200 {object} map[string]uint +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/import/start [post] +func (b *BaseApi) StartVMIsoImport(c *gin.Context) { + var req dto.VMIsoImportStart + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + id, err := vmService.StartIsoImport(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, map[string]uint{"id": id}) +} + +// @Tags VirtualMachine +// @Summary Update virtual machine ISO image import status +// @Accept json +// @Param request body dto.VMIsoImportUpdate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/import/update [post] +func (b *BaseApi) UpdateVMIsoImport(c *gin.Context) { + var req dto.VMIsoImportUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.UpdateIsoImport(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Update virtual machine ISO image record +// @Accept json +// @Param request body dto.VMIsoUpdate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/update [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新虚拟机镜像 [name] [type]","formatEN":"update VM ISO [name] [type]"} +func (b *BaseApi) UpdateVMIso(c *gin.Context) { + var req dto.VMIsoUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.UpdateIso(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine ISO image +// @Accept json +// @Param request body dto.VMIsoDelete true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/iso/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_isos","output_column":"name","output_value":"name"}],"formatZH":"删除虚拟机镜像 [name]","formatEN":"delete VM ISO [name]"} +func (b *BaseApi) DeleteVMIso(c *gin.Context) { + var req dto.VMIsoDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.DeleteIso(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary List virtual machine networks +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMNetworkItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks/list [post] +func (b *BaseApi) ListVMNetworks(c *gin.Context) { + data, err := vmService.ListNetworks() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary List host network interfaces for virtual machines +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMNetworkInterfaceItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks/interfaces [get] +func (b *BaseApi) ListVMNetworkInterfaces(c *gin.Context) { + data, err := vmService.ListNetworkInterfaces() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary List host network bridges for virtual machines +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMNetworkBridgeItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks/bridges [get] +func (b *BaseApi) ListVMNetworkBridges(c *gin.Context) { + data, err := vmService.ListNetworkBridges() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine network +// @Accept json +// @Param request body dto.VMNetworkCreate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机网络 [name] [type]","formatEN":"create VM network [name] [type]"} +func (b *BaseApi) CreateVMNetwork(c *gin.Context) { + var req dto.VMNetworkCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.CreateNetwork(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Update virtual machine network +// @Accept json +// @Param request body dto.VMNetworkUpdate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_networks","output_column":"name","output_value":"name"}],"formatZH":"更新虚拟机网络 [name]","formatEN":"update VM network [name]"} +func (b *BaseApi) UpdateVMNetwork(c *gin.Context) { + var req dto.VMNetworkUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.UpdateNetwork(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine network +// @Accept json +// @Param request body dto.VMResourceDelete true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/networks/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_networks","output_column":"name","output_value":"name"}],"formatZH":"删除虚拟机网络 [name]","formatEN":"delete VM network [name]"} +func (b *BaseApi) DeleteVMNetwork(c *gin.Context) { + var req dto.VMResourceDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.DeleteNetwork(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary List virtual machine storage pools +// @Accept json +// @Produce json +// @Success 200 {array} dto.VMStorageItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/storages/list [post] +func (b *BaseApi) ListVMStorages(c *gin.Context) { + data, err := vmService.ListStorages() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags VirtualMachine +// @Summary Create virtual machine storage pool +// @Accept json +// @Param request body dto.VMStorageCreate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/storages [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建虚拟机存储 [name] [type]","formatEN":"create VM storage [name] [type]"} +func (b *BaseApi) CreateVMStorage(c *gin.Context) { + var req dto.VMStorageCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.CreateStorage(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Update virtual machine storage pool +// @Accept json +// @Param request body dto.VMStorageUpdate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/storages/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_storages","output_column":"name","output_value":"name"}],"formatZH":"更新虚拟机存储 [name]","formatEN":"update VM storage [name]"} +func (b *BaseApi) UpdateVMStorage(c *gin.Context) { + var req dto.VMStorageUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.UpdateStorage(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags VirtualMachine +// @Summary Delete virtual machine storage pool +// @Accept json +// @Param request body dto.VMResourceDelete true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /vms/storages/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"vm_storages","output_column":"name","output_value":"name"}],"formatZH":"删除虚拟机存储 [name]","formatEN":"delete VM storage [name]"} +func (b *BaseApi) DeleteVMStorage(c *gin.Context) { + var req dto.VMResourceDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := vmService.DeleteStorage(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +func validateVMName(c *gin.Context, name string) bool { + if re.GetRegex(re.VMNameValidationPattern).MatchString(name) { + return true + } + helper.BadRequest(c, buserr.New("ErrVMInvalidInput")) + return false +} + +var vmConsoleUpgrader = websocket.Upgrader{ + ReadBufferSize: 32 * 1024, + WriteBufferSize: 32 * 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func buildVNCAddress(graphics *dto.VMGraphics) (string, error) { + if graphics == nil || strings.ToLower(graphics.Type) != "vnc" { + return "", errors.New("vnc console is unavailable") + } + port, err := strconv.Atoi(graphics.Port) + if err != nil || port <= 0 { + return "", fmt.Errorf("invalid vnc port: %s", graphics.Port) + } + host := strings.TrimSpace(graphics.Listen) + if host == "" || host == "0.0.0.0" || host == "::" || host == "::0" { + host = "127.0.0.1" + } + return net.JoinHostPort(host, strconv.Itoa(port)), nil +} + +func forwardVNCToWebSocket(wsConn *websocket.Conn, vncConn net.Conn, errCh chan<- error) { + buf := make([]byte, 32*1024) + for { + n, err := vncConn.Read(buf) + if n > 0 { + if writeErr := wsConn.WriteMessage(websocket.BinaryMessage, buf[:n]); writeErr != nil { + errCh <- writeErr + return + } + } + if err != nil { + errCh <- err + return + } + } +} + +func forwardWebSocketToVNC(wsConn *websocket.Conn, vncConn net.Conn, errCh chan<- error) { + for { + messageType, data, err := wsConn.ReadMessage() + if err != nil { + errCh <- err + return + } + if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage { + continue + } + if _, err := vncConn.Write(data); err != nil { + errCh <- err + return + } + } +} + +func writeVMConsoleClose(wsConn *websocket.Conn, message string) { + _ = wsConn.WriteControl( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseTryAgainLater, message), + time.Now().Add(time.Second), + ) +} diff --git a/agent/app/dto/vm.go b/agent/app/dto/vm.go new file mode 100644 index 000000000..99464d0bc --- /dev/null +++ b/agent/app/dto/vm.go @@ -0,0 +1,447 @@ +package dto + +import ( + "time" +) + +type VMSearchResult struct { + PageResult + Limit int `json:"limit"` // Maximum VM count for the target node; 0 means unlimited. +} + +type VMCheckItem struct { + Name string `json:"name"` + Passed bool `json:"passed"` + Message string `json:"message"` + IsEnable bool `json:"isEnable"` + IsExist bool `json:"isExist"` + IsActive bool `json:"isActive"` +} + +type VMEnvironment struct { + Arch string `json:"arch"` + ArchSupported bool `json:"archSupported"` + Ready bool `json:"ready"` + HelperExist bool `json:"helperExist"` + HelperMessage string `json:"helperMessage"` + KVMExist bool `json:"kvmExist"` + KVMMessage string `json:"kvmMessage"` + LibvirtExist bool `json:"libvirtExist"` + LibvirtActive bool `json:"libvirtActive"` + LibvirtMessage string `json:"libvirtMessage"` + StorageExist bool `json:"storageExist"` + StorageMessage string `json:"storageMessage"` +} + +type VMHealthCheckItem struct { + Level string `json:"level"` + Check string `json:"check"` + Reason string `json:"reason,omitempty"` + Params map[string]string `json:"params,omitempty"` + Detail string `json:"detail,omitempty"` + FixCommand string `json:"fixCommand,omitempty"` +} + +type VMHealthCheckResult struct { + Pass int `json:"pass"` + Warn int `json:"warn"` + Fail int `json:"fail"` + Items []VMHealthCheckItem `json:"items"` +} + +type VMCreate struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + CPU int `json:"cpu" validate:"required,min=1"` + Memory int64 `json:"memory" validate:"required,min=1"` + DiskSize int64 `json:"diskSize" validate:"required,min=1"` + OSType string `json:"osType" validate:"omitempty,oneof=windows linux other"` + DiskBus string `json:"diskBus" validate:"omitempty,oneof=sata virtio"` + TemplateID uint `json:"templateID"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + ISOPaths []string `json:"isoPaths"` + ImagePath string `json:"imagePath"` + Network string `json:"network" validate:"required,vm_common"` + BootOrder string `json:"bootOrder"` + StartAfterCreate bool `json:"startAfterCreate"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` +} + +type VMTemplateCreate struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + TemplateName string `json:"templateName" validate:"required,vm_name"` + StoragePool string `json:"storagePool"` + Description string `json:"description"` +} + +type VMTemplateDelete struct { + ID uint `json:"id" validate:"required"` + DeleteDisk bool `json:"deleteDisk"` + TaskID string `json:"taskID"` +} + +type VMTemplateInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + SourceVM string `json:"sourceVM"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + DiskSize int64 `json:"diskSize"` + DiskBus string `json:"diskBus"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + Network string `json:"network"` + BootOrder string `json:"bootOrder"` + IsExist bool `json:"isExist"` + Size int64 `json:"size"` + CreatedAt time.Time `json:"createdAt"` + Description string `json:"description"` +} + +type VMUpdate struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + CPU int `json:"cpu" validate:"required,min=1"` + Memory int64 `json:"memory" validate:"required,min=1"` + ISOPaths []string `json:"isoPaths"` + Network string `json:"network" validate:"required,vm_common"` + BootOrder string `json:"bootOrder"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` +} + +type VMDiskResize struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + Target string `json:"target" validate:"required,vm_common"` + SizeGiB int64 `json:"sizeGiB" validate:"required,min=1,max=8388607"` +} + +type VMRename struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + NewName string `json:"newName" validate:"required,vm_name"` +} + +type VMDelete struct { + TaskID string `json:"taskID"` + ID uint `json:"id" validate:"required"` + Force bool `json:"force"` +} + +type VMOperate struct { + Name string `json:"name" validate:"required,vm_name"` + Operate string `json:"operate" validate:"required,oneof=start shutdown reboot destroy suspend resume"` +} + +type VMInfoReq struct { + Name string `json:"name" validate:"required,vm_name"` + CheckDisks bool `json:"checkDisks"` +} + +type VMReq struct { + Name string `json:"name" validate:"required,vm_name"` +} + +type VMTaskResp struct { + TaskID string `json:"taskID"` +} + +type VMOrphanItem struct { + Type string `json:"type" validate:"required,oneof=orphan_domain_disk orphan_snapshot partial_file"` + Path string `json:"path" validate:"required"` + Size int64 `json:"size"` + ModifiedAt int64 `json:"modifiedAt"` +} + +type VMOrphanClean struct { + Items []VMOrphanItem `json:"items" validate:"required,min=1,dive"` +} + +type VMSnapshotReq struct { + TaskID string `json:"taskID"` + Name string `json:"name" validate:"required,vm_name"` + SnapshotName string `json:"snapshotName" validate:"required,vm_name"` + Description string `json:"description"` +} + +type VMSnapshotSearch struct { + Name string `json:"name" validate:"required,vm_name"` +} + +type VMSnapshotInfo struct { + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"createdAt"` + State string `json:"state"` + Parent string `json:"parent"` + Path string `json:"path"` + Size int64 `json:"size"` + Current bool `json:"current"` + Children int `json:"children"` +} + +type VMIsoSearch struct { + PageInfo + Type string `json:"type" validate:"required"` + Info string `json:"info"` +} + +type VMIsoCreate struct { + Type string `json:"type" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Description string `json:"description"` +} + +type VMIsoUpdate struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Description string `json:"description"` +} + +type VMIsoImportStart struct { + Type string `json:"type" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Status string `json:"status" validate:"required,oneof=uploading downloading"` + TempFileID string `json:"tempFileID"` + TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"` + Description string `json:"description"` +} + +type VMIsoImportUpdate struct { + ID uint `json:"id" validate:"required"` + Status string `json:"status" validate:"required,oneof=uploading downloading ready failed"` + Error string `json:"error"` + TempFileID string `json:"tempFileID"` + TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"` +} + +type VMIsoDelete struct { + ID uint `json:"id" validate:"required"` + DeleteFile bool `json:"deleteFile"` +} + +type VMIsoInfo struct { + ID uint `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Path string `json:"path"` + IsExist bool `json:"isExist"` + Size int64 `json:"size"` + Status string `json:"status"` + Error string `json:"error"` + TempFileID string `json:"tempFileID"` + TempFileType string `json:"tempFileType"` + CreatedAt time.Time `json:"createdAt"` + Description string `json:"description"` +} + +type VMResourceInfo struct { + Type string `json:"type"` + Name string `json:"name"` + SubType string `json:"subType"` + BridgeName string `json:"bridgeName"` + SourceInterface string `json:"sourceInterface"` + CIDR string `json:"cidr"` + Gateway string `json:"gateway"` + DHCPStart string `json:"dhcpStart"` + DHCPEnd string `json:"dhcpEnd"` + Path string `json:"path"` + Status string `json:"status"` + AutoStart bool `json:"autoStart"` + Capacity uint64 `json:"capacity"` + Available uint64 `json:"available"` +} + +type VMResourceDelete struct { + ID uint `json:"id" validate:"required"` + Force bool `json:"force"` +} + +type VMNetworkCreate struct { + Name string `json:"name" validate:"required,vm_common"` + Type string `json:"type" validate:"required,oneof=nat bridge macvtap"` + BridgeName string `json:"bridgeName"` + SourceInterface string `json:"sourceInterface" validate:"required_if=Type macvtap"` + CIDR string `json:"cidr"` + Gateway string `json:"gateway"` + DHCPStart string `json:"dhcpStart"` + DHCPEnd string `json:"dhcpEnd"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` +} + +type VMNetworkUpdate struct { + ID uint `json:"id" validate:"required"` + AutoStart *bool `json:"autoStart"` + Status *string `json:"status"` + Description *string `json:"description"` +} + +type VMNetworkItem struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + BridgeName string `json:"bridgeName"` + SourceInterface string `json:"sourceInterface"` + CIDR string `json:"cidr"` + Gateway string `json:"gateway"` + DHCPStart string `json:"dhcpStart"` + DHCPEnd string `json:"dhcpEnd"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` + CreatedAt string `json:"createdAt"` + Status string `json:"status"` + VMCount int `json:"vmCount"` + VMs []VMResourceRef `json:"vms"` + Registered bool `json:"registered"` + UsedIPs []VMNetworkIPUsage `json:"usedIPs"` + Error string `json:"error"` +} + +type VMResourceRef struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type VMNetworkIPUsage struct { + IP string `json:"ip"` + VMName string `json:"vmName"` + MAC string `json:"mac"` + Source string `json:"source"` +} + +type VMNetworkInterfaceItem struct { + Name string `json:"name"` + Addresses []string `json:"addresses"` +} + +type VMNetworkBridgeItem struct { + Name string `json:"name"` + Recommended bool `json:"recommended"` + Description string `json:"description"` +} + +type VMStorageCreate struct { + Name string `json:"name" validate:"required,vm_common"` + Type string `json:"type" validate:"required,oneof=dir"` + Path string `json:"path" validate:"required"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` +} + +type VMStorageUpdate struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"omitempty,vm_common"` + Path string `json:"path"` + AutoStart *bool `json:"autoStart"` + Status *string `json:"status"` + Description *string `json:"description"` +} + +type VMStorageItem struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Path string `json:"path"` + AutoStart bool `json:"autoStart"` + Description string `json:"description"` + CreatedAt string `json:"createdAt"` + Status string `json:"status"` + Capacity uint64 `json:"capacity"` + Available uint64 `json:"available"` + VMCount int `json:"vmCount"` + VMs []VMResourceRef `json:"vms"` + Error string `json:"error"` +} + +type VMInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + UUID string `json:"uuid"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + DiskSize int64 `json:"diskSize"` + DiskBus string `json:"diskBus"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + ISOPaths []string `json:"isoPaths"` + ImagePath string `json:"imagePath"` + Network string `json:"network"` + BootOrder string `json:"bootOrder"` + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + Disks []VMDisk `json:"disks"` + Interfaces []VMInterface `json:"interfaces"` + Graphics *VMGraphics `json:"graphics"` + TaskID string `json:"taskID"` + CreateTaskID string `json:"createTaskID"` + Description string `json:"description"` +} + +type VMDisk struct { + Capacity int64 `json:"capacity"` + Format string `json:"format"` + ReadOnly bool `json:"readOnly"` + Shareable bool `json:"shareable"` + ResizeBlocked string `json:"resizeBlocked"` + Device string `json:"device"` + Type string `json:"type"` + Source string `json:"source"` + Target string `json:"target"` + Bus string `json:"bus"` +} + +type VMInterface struct { + Type string `json:"type"` + Source string `json:"source"` + Model string `json:"model"` + MAC string `json:"mac"` + IP string `json:"ip"` +} + +type VMGraphics struct { + Type string `json:"type"` + Port string `json:"port"` + Listen string `json:"listen"` +} + +type VMStats struct { + Name string `json:"name"` + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + CPUTime uint64 `json:"cpuTime"` + VCPU int `json:"vcpu"` + Memory VMMemoryStats `json:"memory"` + Disk VMDiskStats `json:"disk"` + Network VMNetworkStats `json:"network"` +} + +type VMMemoryStats struct { + AvailableKiB uint64 `json:"availableKiB"` + UnusedKiB uint64 `json:"unusedKiB"` + UsableKiB uint64 `json:"usableKiB"` + RSSKiB uint64 `json:"rssKiB"` + ActualKiB uint64 `json:"actualKiB"` +} + +type VMDiskStats struct { + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` + ReadReqs uint64 `json:"readReqs"` + WriteReqs uint64 `json:"writeReqs"` +} + +type VMNetworkStats struct { + RxBytes uint64 `json:"rxBytes"` + TxBytes uint64 `json:"txBytes"` + RxPackets uint64 `json:"rxPackets"` + TxPackets uint64 `json:"txPackets"` +} diff --git a/agent/app/model/vm.go b/agent/app/model/vm.go new file mode 100644 index 000000000..3c46c1c38 --- /dev/null +++ b/agent/app/model/vm.go @@ -0,0 +1,81 @@ +package model + +type VirtualMachine struct { + BaseModel + + Name string `json:"name"` + UUID string `json:"uuid"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + DiskSize int64 `json:"diskSize"` + DiskBus string `json:"diskBus"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + ISOPaths []string `json:"isoPaths" gorm:"serializer:json"` + ImagePath string `json:"imagePath"` + Network string `json:"network"` + BootOrder string `json:"bootOrder"` + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + TaskID string `json:"taskID"` + CreateTaskID string `json:"createTaskID"` + Description string `json:"description"` +} + +type VMTemplate struct { + BaseModel + + Name string `json:"name"` + SourceVM string `json:"sourceVM"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + DiskSize int64 `json:"diskSize"` + DiskBus string `json:"diskBus"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + Network string `json:"network"` + BootOrder string `json:"bootOrder"` + Description string `json:"description"` +} + +type VMIso struct { + BaseModel + + Type string `json:"type"` + Name string `json:"name"` + Path string `json:"path"` + Status string `json:"status"` + Error string `json:"error"` + TempFileID string `json:"tempFileID"` + TempFileType string `json:"tempFileType"` + Description string `json:"description"` +} + +type VMNetwork struct { + BaseModel + + Name string `json:"name"` + Type string `json:"type"` + BridgeName string `json:"bridgeName"` + SourceInterface string `json:"sourceInterface"` + CIDR string `json:"cidr" gorm:"column:cidr"` + Gateway string `json:"gateway"` + DHCPStart string `json:"dhcpStart"` + DHCPEnd string `json:"dhcpEnd"` + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + Description string `json:"description"` + Error string `json:"error"` +} + +type VMStorage struct { + BaseModel + + Name string `json:"name"` + Type string `json:"type"` + Path string `json:"path"` + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + Description string `json:"description"` + Error string `json:"error"` +} diff --git a/agent/app/repo/vm.go b/agent/app/repo/vm.go new file mode 100644 index 000000000..6411144eb --- /dev/null +++ b/agent/app/repo/vm.go @@ -0,0 +1,315 @@ +package repo + +import ( + "encoding/json" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type VMRepo struct{} + +type IVMRepo interface { + Get(opts ...DBOption) (model.VirtualMachine, error) + List() ([]model.VirtualMachine, error) + Count() (int64, error) + Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error) + Create(vm *model.VirtualMachine) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + WithByNetwork(network string) DBOption + WithByStorage(storage string) DBOption + + GetTemplate(opts ...DBOption) (model.VMTemplate, error) + ListTemplates(opts ...DBOption) ([]model.VMTemplate, error) + CreateTemplate(item *model.VMTemplate) error + UpdateTemplateSource(oldName string, newName string) error + DeleteTemplate(opts ...DBOption) error + + GetIso(opts ...DBOption) (model.VMIso, error) + PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error) + ListIsos(opts ...DBOption) ([]model.VMIso, error) + CreateIso(iso *model.VMIso) error + UpdateIso(id uint, vars map[string]interface{}) error + DeleteIso(opts ...DBOption) error + + GetNetwork(opts ...DBOption) (model.VMNetwork, error) + PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error) + ListNetworks(opts ...DBOption) ([]model.VMNetwork, error) + CreateNetwork(network *model.VMNetwork) error + UpdateNetwork(id uint, vars map[string]interface{}) error + DeleteNetwork(opts ...DBOption) error + + GetStorage(opts ...DBOption) (model.VMStorage, error) + PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error) + ListStorages(opts ...DBOption) ([]model.VMStorage, error) + CreateStorage(storage *model.VMStorage) error + UpdateStorage(id uint, vars map[string]interface{}) error + DeleteStorage(opts ...DBOption) error +} + +func NewIVMRepo() IVMRepo { + return &VMRepo{} +} + +func (v *VMRepo) Get(opts ...DBOption) (model.VirtualMachine, error) { + var vm model.VirtualMachine + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&vm).Error + return vm, err +} + +func (v *VMRepo) List() ([]model.VirtualMachine, error) { + var vms []model.VirtualMachine + err := global.DB.Order("created_at desc").Find(&vms).Error + return vms, err +} + +func (v *VMRepo) Count() (int64, error) { + var count int64 + err := global.DB.Model(&model.VirtualMachine{}).Count(&count).Error + return count, err +} + +func (v *VMRepo) Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error) { + var vms []model.VirtualMachine + var total int64 + db := global.DB.Model(&model.VirtualMachine{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Count(&total).Error + if err != nil { + return 0, nil, err + } + err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&vms).Error + return total, vms, err +} + +func (v *VMRepo) Create(vm *model.VirtualMachine) error { + return global.DB.Create(vm).Error +} + +func (v *VMRepo) Update(id uint, vars map[string]interface{}) error { + if paths, ok := vars["iso_paths"].([]string); ok { + data, err := json.Marshal(paths) + if err != nil { + return err + } + vars["iso_paths"] = string(data) + } + return global.DB.Model(&model.VirtualMachine{}).Where("id = ?", id).Updates(vars).Error +} + +func (v *VMRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.VirtualMachine{}).Error +} + +func (v *VMRepo) WithByNetwork(network string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("network = ?", network) + } +} + +func (v *VMRepo) WithByStorage(storage string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("storage_pool = ?", storage) + } +} + +func (v *VMRepo) GetTemplate(opts ...DBOption) (model.VMTemplate, error) { + var item model.VMTemplate + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&item).Error + return item, err +} + +func (v *VMRepo) ListTemplates(opts ...DBOption) ([]model.VMTemplate, error) { + var items []model.VMTemplate + db := global.DB.Model(&model.VMTemplate{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Order("created_at desc").Order("id desc").Find(&items).Error + return items, err +} + +func (v *VMRepo) CreateTemplate(item *model.VMTemplate) error { + return global.DB.Create(item).Error +} + +func (v *VMRepo) UpdateTemplateSource(oldName string, newName string) error { + return global.DB.Model(&model.VMTemplate{}).Where("source_vm = ?", oldName).Update("source_vm", newName).Error +} + +func (v *VMRepo) DeleteTemplate(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.VMTemplate{}).Error +} + +func (v *VMRepo) GetIso(opts ...DBOption) (model.VMIso, error) { + var iso model.VMIso + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&iso).Error + return iso, err +} + +func (v *VMRepo) PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error) { + var isos []model.VMIso + var total int64 + db := global.DB.Model(&model.VMIso{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Count(&total).Error + if err != nil { + return 0, nil, err + } + err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&isos).Error + return total, isos, err +} + +func (v *VMRepo) ListIsos(opts ...DBOption) ([]model.VMIso, error) { + var isos []model.VMIso + db := global.DB.Model(&model.VMIso{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Order("created_at desc").Order("id desc").Find(&isos).Error + return isos, err +} + +func (v *VMRepo) CreateIso(iso *model.VMIso) error { + return global.DB.Create(iso).Error +} + +func (v *VMRepo) UpdateIso(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.VMIso{}).Where("id = ?", id).Updates(vars).Error +} + +func (v *VMRepo) DeleteIso(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.VMIso{}).Error +} + +func (v *VMRepo) GetNetwork(opts ...DBOption) (model.VMNetwork, error) { + var network model.VMNetwork + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&network).Error + return network, err +} + +func (v *VMRepo) PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error) { + var networks []model.VMNetwork + var total int64 + db := global.DB.Model(&model.VMNetwork{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Count(&total).Error + if err != nil { + return 0, nil, err + } + err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&networks).Error + return total, networks, err +} + +func (v *VMRepo) ListNetworks(opts ...DBOption) ([]model.VMNetwork, error) { + var networks []model.VMNetwork + db := global.DB.Model(&model.VMNetwork{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Order("created_at desc").Order("id desc").Find(&networks).Error + return networks, err +} + +func (v *VMRepo) CreateNetwork(network *model.VMNetwork) error { + return global.DB.Create(network).Error +} + +func (v *VMRepo) UpdateNetwork(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.VMNetwork{}).Where("id = ?", id).Updates(vars).Error +} + +func (v *VMRepo) DeleteNetwork(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.VMNetwork{}).Error +} + +func (v *VMRepo) GetStorage(opts ...DBOption) (model.VMStorage, error) { + var storage model.VMStorage + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&storage).Error + return storage, err +} + +func (v *VMRepo) PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error) { + var storages []model.VMStorage + var total int64 + db := global.DB.Model(&model.VMStorage{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Count(&total).Error + if err != nil { + return 0, nil, err + } + err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&storages).Error + return total, storages, err +} + +func (v *VMRepo) ListStorages(opts ...DBOption) ([]model.VMStorage, error) { + var storages []model.VMStorage + db := global.DB.Model(&model.VMStorage{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Order("created_at desc").Order("id desc").Find(&storages).Error + return storages, err +} + +func (v *VMRepo) CreateStorage(storage *model.VMStorage) error { + return global.DB.Create(storage).Error +} + +func (v *VMRepo) UpdateStorage(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.VMStorage{}).Where("id = ?", id).Updates(vars).Error +} + +func (v *VMRepo) DeleteStorage(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.VMStorage{}).Error +} diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index f48a111ca..7839da047 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -3,6 +3,7 @@ package service import "github.com/1Panel-dev/1Panel/agent/app/repo" var ( + vmRepo = repo.NewIVMRepo() modelDownloadTaskRepo = repo.NewIModelDownloadTaskRepo() featuredModelRepo = repo.NewIFeaturedModelRepo() appRepo = repo.NewIAppRepo() diff --git a/agent/app/service/vm-iso.go b/agent/app/service/vm-iso.go new file mode 100644 index 000000000..e32374c23 --- /dev/null +++ b/agent/app/service/vm-iso.go @@ -0,0 +1,171 @@ +package service + +import ( + "os" + "path/filepath" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/jinzhu/copier" +) + +func (v *VMService) SearchIso(req dto.SearchPageWithType) (int64, interface{}, error) { + options := []repo.DBOption{repo.WithOrderDesc("created_at"), repo.WithOrderDesc("id")} + if req.Type != "" { + options = append(options, repo.WithByType(req.Type)) + } + if req.Info != "" { + options = append(options, repo.WithByLikeName(req.Info)) + } + total, isos, err := vmRepo.PageIso(req.Page, req.PageSize, options...) + if err != nil { + return 0, nil, err + } + dtoIsos := make([]dto.VMIsoInfo, 0, len(isos)) + for _, iso := range isos { + var item dto.VMIsoInfo + if err := copier.Copy(&item, &iso); err != nil { + return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) + } + item.IsExist, item.Size = loadVMResourceFileInfo(item.Path) + dtoIsos = append(dtoIsos, item) + } + return total, dtoIsos, err +} + +func (v *VMService) ListIso() ([]dto.VMIsoInfo, error) { + isos, err := vmRepo.ListIsos() + if err != nil { + return nil, err + } + dtoIsos := make([]dto.VMIsoInfo, 0, len(isos)) + for _, iso := range isos { + if iso.Status == "uploading" || iso.Status == "downloading" || iso.Status == "failed" { + continue + } + var item dto.VMIsoInfo + if err := copier.Copy(&item, &iso); err != nil { + return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) + } + item.IsExist, item.Size = loadVMResourceFileInfo(item.Path) + dtoIsos = append(dtoIsos, item) + } + return dtoIsos, nil +} + +func (v *VMService) CreateIso(req dto.VMIsoCreate) error { + isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name)) + if isoItem.ID != 0 { + return buserr.New("ErrRecordExist") + } + exists, _ := loadVMResourceFileInfo(req.Path) + if !exists { + return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil) + } + return vmRepo.CreateIso(&model.VMIso{ + Type: req.Type, Name: req.Name, Path: req.Path, Status: "ready", Description: req.Description, + }) +} + +func (v *VMService) StartIsoImport(req dto.VMIsoImportStart) (uint, error) { + isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name)) + if isoItem.ID != 0 { + return 0, buserr.New("ErrRecordExist") + } + item := model.VMIso{Type: req.Type, Name: req.Name, Path: req.Path, Status: req.Status, TempFileID: req.TempFileID, TempFileType: req.TempFileType, Description: req.Description} + if err := vmRepo.CreateIso(&item); err != nil { + return 0, err + } + return item.ID, nil +} + +func (v *VMService) UpdateIsoImport(req dto.VMIsoImportUpdate) error { + isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID)) + if err != nil || isoItem.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + if isoItem.Status != "uploading" && isoItem.Status != "downloading" { + return buserr.WithDetail("ErrVMInvalidInput", "ISO import is not active", nil) + } + if (isoItem.Status == "uploading" && req.Status == "downloading") || + (isoItem.Status == "downloading" && req.Status == "uploading") { + return buserr.WithDetail("ErrVMInvalidInput", "ISO import type cannot be changed", nil) + } + vars := map[string]interface{}{"status": req.Status, "error": req.Error} + if req.TempFileID != "" { + vars["temp_file_id"] = req.TempFileID + } + if req.TempFileType != "" { + vars["temp_file_type"] = req.TempFileType + } + if req.Status == "ready" { + exists, _ := loadVMResourceFileInfo(isoItem.Path) + if !exists { + return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil) + } + vars["error"] = "" + } + if req.Status == "ready" { + vars["temp_file_id"] = "" + vars["temp_file_type"] = "" + } + return vmRepo.UpdateIso(req.ID, vars) +} + +func (v *VMService) UpdateIso(req dto.VMIsoUpdate) error { + isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID)) + if err != nil { + return err + } + if isoItem.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + exists, _ := loadVMResourceFileInfo(req.Path) + if !exists { + return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil) + } + return vmRepo.UpdateIso(req.ID, map[string]interface{}{ + "name": req.Name, "type": req.Type, "path": req.Path, "description": req.Description, + }) +} + +func (v *VMService) DeleteIso(req dto.VMIsoDelete) error { + isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID)) + if err != nil { + return err + } + if isoItem.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + if req.DeleteFile && isoItem.Path != "" { + exists, _ := loadVMResourceFileInfo(isoItem.Path) + if exists { + if err := os.Remove(isoItem.Path); err != nil && !os.IsNotExist(err) { + return err + } + } + } + if isoItem.TempFileType == "upload" && validTempFileID(isoItem.TempFileID) { + tmpPath := filepath.Join(global.CONF.Base.InstallDir, "1panel", "tmp", "upload", isoItem.TempFileID) + if err := os.RemoveAll(tmpPath); err != nil { + return err + } + } + return vmRepo.DeleteIso(repo.WithByID(req.ID)) +} + +func loadVMResourceFileInfo(filePath string) (bool, int64) { + info, err := os.Stat(filePath) + if err != nil || info.IsDir() { + return false, 0 + } + return true, info.Size() +} + +func validTempFileID(value string) bool { + return value != "" && filepath.Base(value) == value && !strings.ContainsAny(value, `/\\`) +} diff --git a/agent/app/service/vm-network.go b/agent/app/service/vm-network.go new file mode 100644 index 000000000..4e88ddacd --- /dev/null +++ b/agent/app/service/vm-network.go @@ -0,0 +1,464 @@ +package service + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/vm/types" + "gorm.io/gorm" +) + +func (v *VMService) ListNetworks() ([]dto.VMNetworkItem, error) { + records, err := vmRepo.ListNetworks() + if err != nil { + return nil, err + } + items := v.networkRecordsToItems(records) + return items, nil +} + +func (v *VMService) ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) { + return listNetworkBridges("/sys/class/net") +} + +func (v *VMService) ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + return listMacvtapInterfaces("/sys/class/net", interfaces) +} + +func (v *VMService) CreateNetwork(req dto.VMNetworkCreate) error { + if req.Type == "macvtap" { + interfaces, err := v.ListNetworkInterfaces() + if err != nil { + return err + } + found := false + for _, item := range interfaces { + if item.Name == req.SourceInterface { + found = true + break + } + } + if !found { + return fmt.Errorf("unavailable macvtap source interface: %s", req.SourceInterface) + } + } else { + req.SourceInterface = "" + } + if req.Type != "nat" { + req.CIDR, req.Gateway, req.DHCPStart, req.DHCPEnd = "", "", "", "" + } + if req.Type == "macvtap" { + req.BridgeName = "" + } + ctx, cancel := vmResourceOperationContext(false) + defer cancel() + unlock, err := vmResourceLocks.lock(ctx, "network", req.Name) + if err != nil { + return err + } + defer unlock() + itemNetwork, err := vmRepo.GetNetwork(repo.WithByName(req.Name)) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if itemNetwork.ID != 0 { + return buserr.New("ErrRecordExist") + } + if req.Type == "bridge" && isManagedNetworkBridge(req.BridgeName) { + return buserr.New("ErrVMBridgeManaged") + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + if _, exists, err := v.findVMResourceWithContext(ctx, "network", req.Name); err != nil { + return err + } else if exists { + return buserr.New("ErrRecordExist") + } + record := model.VMNetwork{ + Name: req.Name, Type: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface, + CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd, + AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description, + } + if err := vmRepo.CreateNetwork(&record); err != nil { + return err + } + if err := v.provider.CreateResource(ctx, types.VMResourceCreate{ + Type: "network", Name: req.Name, SubType: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface, + CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd, + AutoStart: req.AutoStart, + }); err != nil { + if updateErr := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{ + "status": constant.StatusFailed, "error": err.Error(), + }); updateErr != nil { + return errors.Join(err, fmt.Errorf("save network creation failure: %w", updateErr)) + } + return err + } + return vmRepo.UpdateNetwork(record.ID, map[string]interface{}{ + "status": constant.StatusRunning, "error": "", + }) +} + +func (v *VMService) SyncNetworks() error { + revision, idle := vmResourceLocks.snapshot("network") + if !idle { + return nil + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + liveItems, err := v.provider.ListResources(context.Background(), "network") + if err != nil { + return err + } + return vmResourceLocks.applySnapshot("network", revision, func() error { + records, err := vmRepo.ListNetworks() + if err != nil { + return err + } + liveByName := map[string]dto.VMResourceInfo{} + for _, live := range liveItems { + if live.Name == "" { + continue + } + liveByName[live.Name] = live + } + recordsByName := map[string]model.VMNetwork{} + for _, record := range records { + if vmResourceStatusEquals(record.Status, vmResourceCreating) { + record.Status = constant.StatusFailed + record.Error = "Network creation was interrupted before its result was saved" + if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{ + "status": record.Status, "error": record.Error, + }); err != nil { + return err + } + } + recordsByName[record.Name] = record + } + for name, live := range liveByName { + vars := map[string]interface{}{ + "type": live.SubType, + "bridge_name": live.BridgeName, + "source_interface": live.SourceInterface, + "cidr": live.CIDR, + "gateway": live.Gateway, + "dhcp_start": live.DHCPStart, + "dhcp_end": live.DHCPEnd, + "auto_start": live.AutoStart, + "status": normalizeVMResourceStatus(live.Status), + } + if record, ok := recordsByName[name]; ok { + if vmResourceStatusEquals(record.Status, constant.StatusFailed) { + continue + } + if err := vmRepo.UpdateNetwork(record.ID, vars); err != nil { + return err + } + continue + } + if err := vmRepo.CreateNetwork(&model.VMNetwork{ + Name: live.Name, Type: live.SubType, BridgeName: live.BridgeName, SourceInterface: live.SourceInterface, + CIDR: live.CIDR, Gateway: live.Gateway, DHCPStart: live.DHCPStart, DHCPEnd: live.DHCPEnd, + AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), + }); err != nil { + return err + } + } + for _, record := range recordsByName { + if vmResourceStatusEquals(record.Status, constant.StatusFailed) { + continue + } + if _, ok := liveByName[record.Name]; !ok { + if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{ + "status": constant.StatusDeleted, + }); err != nil { + return err + } + } + } + return nil + }) +} + +func (v *VMService) UpdateNetwork(req dto.VMNetworkUpdate) error { + ctx, cancel := vmResourceOperationContext(false) + defer cancel() + itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID)) + if err != nil { + return err + } + unlock, err := vmResourceLocks.lock(ctx, "network", itemNetwork.Name) + if err != nil { + return err + } + defer unlock() + itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID)) + if err != nil { + return err + } + if itemNetwork.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + vars := map[string]interface{}{} + if req.Description != nil { + vars["description"] = *req.Description + } + if req.AutoStart != nil || req.Status != nil { + if vmResourceCreationIncomplete(itemNetwork.Status) { + return buserr.WithDetail("ErrInvalidParams", "network creation has not succeeded", nil) + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + live, ok, err := v.findVMResourceWithContext(ctx, "network", itemNetwork.Name) + if err != nil { + return err + } + if !ok { + _ = vmRepo.UpdateNetwork(req.ID, map[string]interface{}{"status": constant.StatusDeleted}) + return buserr.WithDetail("ErrResourceNotExists", "Network does not exist", nil) + } + if live.Status != "" { + vars["status"] = normalizeVMResourceStatus(live.Status) + } + } + if req.AutoStart != nil { + if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{ + Type: "network", Name: itemNetwork.Name, AutoStart: req.AutoStart, + }); err != nil { + return err + } + vars["auto_start"] = *req.AutoStart + } + if req.Status != nil { + status := normalizeVMResourceStatus(*req.Status) + if !validVMResourceOperateStatus(status) { + return buserr.WithDetail("ErrInvalidParams", status, nil) + } + if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{ + Type: "network", Name: itemNetwork.Name, Status: &status, + }); err != nil { + return err + } + vars["status"] = status + } + if len(vars) == 0 { + return nil + } + return vmRepo.UpdateNetwork(req.ID, vars) +} + +func (v *VMService) DeleteNetwork(req dto.VMResourceDelete) error { + ctx, cancel := vmResourceOperationContext(req.Force) + defer cancel() + itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return buserr.New("ErrRecordNotFound") + } + return err + } + unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "network", itemNetwork.Name, req.Force) + if err != nil { + return err + } + defer unlock() + itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID)) + if err != nil { + return err + } + if len(loadVMResourceRefs("network")[itemNetwork.Name]) > 0 { + return buserr.New("ErrVMNetworkInUse") + } + if cleanup { + if err := v.deleteVMResource(ctx, "network", itemNetwork.Name, req.Force); err != nil { + return err + } + } + return vmRepo.DeleteNetwork(repo.WithByID(req.ID)) +} + +func (v *VMService) networkRecordsToItems(records []model.VMNetwork) []dto.VMNetworkItem { + items := make([]dto.VMNetworkItem, 0, len(records)) + vmRefs := loadVMResourceRefs("network") + usedIPs := v.loadVMNetworkIPUsages(records) + for _, record := range records { + status := normalizeVMResourceStatus(record.Status) + items = append(items, dto.VMNetworkItem{ + ID: record.ID, Name: record.Name, Type: record.Type, + BridgeName: record.BridgeName, SourceInterface: record.SourceInterface, CIDR: record.CIDR, Gateway: record.Gateway, + DHCPStart: record.DHCPStart, DHCPEnd: record.DHCPEnd, AutoStart: record.AutoStart, + Description: record.Description, CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"), + Status: status, Error: record.Error, VMCount: len(vmRefs[record.Name]), VMs: vmRefs[record.Name], + Registered: true, UsedIPs: usedIPs[record.Name], + }) + } + return items +} + +func (v *VMService) loadVMNetworkIPUsages(records []model.VMNetwork) map[string][]dto.VMNetworkIPUsage { + usages := map[string][]dto.VMNetworkIPUsage{} + if !v.helperExists() { + return usages + } + domains, err := v.provider.List(context.Background()) + if err != nil { + return usages + } + vmRecords, err := vmRepo.List() + if err != nil { + return usages + } + vmRecordsByName := map[string]model.VirtualMachine{} + for _, record := range vmRecords { + vmRecordsByName[record.Name] = record + } + networkNames := map[string]struct{}{} + bridgeToNetwork := map[string]string{} + for _, record := range records { + networkNames[record.Name] = struct{}{} + if record.BridgeName != "" { + bridgeToNetwork[record.BridgeName] = record.Name + } + } + for _, domain := range domains { + if record, ok := vmRecordsByName[domain.Name]; ok { + mergeVMMetadata(&domain, record) + } + for _, item := range domain.Interfaces { + ip := strings.TrimSpace(item.IP) + if ip == "" { + continue + } + networkName := resolveVMInterfaceNetwork(item.Source, domain.Network, networkNames, bridgeToNetwork) + if networkName == "" { + continue + } + usages[networkName] = append(usages[networkName], dto.VMNetworkIPUsage{ + IP: ip, VMName: domain.Name, MAC: item.MAC, Source: item.Source, + }) + } + } + for name := range usages { + sort.Slice(usages[name], func(i, j int) bool { + if usages[name][i].IP != usages[name][j].IP { + return usages[name][i].IP < usages[name][j].IP + } + return strings.ToLower(usages[name][i].VMName) < strings.ToLower(usages[name][j].VMName) + }) + } + return usages +} + +func resolveVMInterfaceNetwork(source, vmNetwork string, networkNames map[string]struct{}, bridgeToNetwork map[string]string) string { + if _, ok := networkNames[source]; ok { + return source + } + if _, ok := networkNames[vmNetwork]; ok { + return vmNetwork + } + return bridgeToNetwork[source] +} + +func listNetworkBridges(netClassPath string) ([]dto.VMNetworkBridgeItem, error) { + entries, err := os.ReadDir(netClassPath) + if err != nil { + if os.IsNotExist(err) { + return []dto.VMNetworkBridgeItem{}, nil + } + return nil, err + } + + items := make([]dto.VMNetworkBridgeItem, 0) + for _, entry := range entries { + name := entry.Name() + netPath := netClassPath + "/" + name + info, err := os.Stat(netPath) + if err != nil || !info.IsDir() { + continue + } + if _, err := os.Stat(netPath + "/bridge"); err != nil { + continue + } + item := dto.VMNetworkBridgeItem{ + Name: name, + Recommended: true, + } + switch { + case name == "docker0" || strings.HasPrefix(name, "br-"): + item.Recommended = false + item.Description = "docker" + case name == "virbr0" || strings.HasPrefix(name, "virbr"): + item.Recommended = false + item.Description = "libvirt" + } + items = append(items, item) + } + sort.Slice(items, func(i, j int) bool { + if items[i].Recommended != items[j].Recommended { + return items[i].Recommended + } + return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name) + }) + return items, nil +} + +func isManagedNetworkBridge(name string) bool { + return name == "docker0" || strings.HasPrefix(name, "br-") || name == "virbr0" || strings.HasPrefix(name, "virbr") +} + +func listMacvtapInterfaces(netClassPath string, interfaces []net.Interface) ([]dto.VMNetworkInterfaceItem, error) { + items := make([]dto.VMNetworkInterfaceItem, 0) + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) != 6 { + continue + } + netPath := filepath.Join(netClassPath, iface.Name) + if _, err := os.Stat(filepath.Join(netPath, "device")); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + continue + } + excluded := false + for _, name := range []string{"master", "wireless", "bridge"} { + if _, err := os.Stat(filepath.Join(netPath, name)); err == nil { + excluded = true + break + } else if !os.IsNotExist(err) { + return nil, err + } + } + if excluded { + continue + } + addresses, err := iface.Addrs() + if err != nil { + return nil, err + } + item := dto.VMNetworkInterfaceItem{Name: iface.Name, Addresses: []string{}} + for _, address := range addresses { + item.Addresses = append(item.Addresses, address.String()) + } + items = append(items, item) + } + sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) + return items, nil +} diff --git a/agent/app/service/vm-resource-delete.go b/agent/app/service/vm-resource-delete.go new file mode 100644 index 000000000..53d93563e --- /dev/null +++ b/agent/app/service/vm-resource-delete.go @@ -0,0 +1,36 @@ +package service + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" +) + +func (v *VMService) deleteVMResource(ctx context.Context, resourceType, name string, force bool) error { + if err := v.deleteVMResourceWithContext(ctx, resourceType, name); err != nil { + if !force { + return err + } + if global.LOG != nil { + global.LOG.Warnf("[xpack] force delete VM %s %q metadata after resource cleanup failed: %v", resourceType, name, err) + } + } + return nil +} + +func (v *VMService) deleteVMResourceWithContext(ctx context.Context, resourceType, name string) error { + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + items, err := v.provider.ListResources(ctx, resourceType) + if err != nil { + return err + } + for _, item := range items { + if item.Name == name { + return v.provider.DeleteResource(ctx, resourceType, name) + } + } + return nil +} diff --git a/agent/app/service/vm-resource-lock.go b/agent/app/service/vm-resource-lock.go new file mode 100644 index 000000000..1e1cd6767 --- /dev/null +++ b/agent/app/service/vm-resource-lock.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +type vmResourceKey struct{ kind, name string } +type vmResourceGate struct { + token chan struct{} + refs int +} + +type vmResourceLockSet struct { + mu sync.Mutex + gates map[vmResourceKey]*vmResourceGate + active map[string]int + revision map[string]uint64 +} + +var vmResourceLocks = newVMResourceLockSet() + +func newVMResourceLockSet() *vmResourceLockSet { + return &vmResourceLockSet{gates: make(map[vmResourceKey]*vmResourceGate), active: make(map[string]int), revision: make(map[string]uint64)} +} +func vmResourceOperationContext(force bool) (context.Context, context.CancelFunc) { + timeout := 2 * time.Minute + if force { + timeout = 10 * time.Second + } + return context.WithTimeout(context.Background(), timeout) +} +func (s *vmResourceLockSet) lock(ctx context.Context, kind, name string) (func(), error) { + key := vmResourceKey{kind, name} + s.mu.Lock() + gate := s.gates[key] + if gate == nil { + gate = &vmResourceGate{token: make(chan struct{}, 1)} + gate.token <- struct{}{} + s.gates[key] = gate + } + gate.refs++ + s.mu.Unlock() + select { + case <-ctx.Done(): + s.mu.Lock() + s.dropRef(key, gate) + s.mu.Unlock() + return nil, ctx.Err() + case <-gate.token: + } + s.mu.Lock() + if err := ctx.Err(); err != nil { + gate.token <- struct{}{} + s.dropRef(key, gate) + s.mu.Unlock() + return nil, err + } + s.active[kind]++ + s.revision[kind]++ + s.mu.Unlock() + return func() { + s.mu.Lock() + s.active[kind]-- + s.revision[kind]++ + gate.token <- struct{}{} + s.dropRef(key, gate) + s.mu.Unlock() + }, nil +} +func (s *vmResourceLockSet) dropRef(key vmResourceKey, gate *vmResourceGate) { + gate.refs-- + if gate.refs == 0 { + delete(s.gates, key) + } +} + +func (s *vmResourceLockSet) lockForDelete(ctx context.Context, kind, name string, force bool) (func(), bool, error) { + unlock, err := s.lock(ctx, kind, name) + if err == nil { + return unlock, true, nil + } + if !force { + return nil, false, err + } + if global.LOG != nil { + global.LOG.Warnf("[xpack] force delete VM %s %q metadata after waiting for resource lock failed: %v", kind, name, err) + } + s.mu.Lock() + s.revision[kind]++ + return func() { s.revision[kind]++; s.mu.Unlock() }, false, nil +} + +func (s *vmResourceLockSet) snapshot(kind string) (uint64, bool) { + s.mu.Lock() + defer s.mu.Unlock() + return s.revision[kind], s.active[kind] == 0 +} + +func (s *vmResourceLockSet) applySnapshot(kind string, revision uint64, apply func() error) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.active[kind] != 0 || s.revision[kind] != revision { + return nil + } + defer func() { s.revision[kind]++ }() + return apply() +} diff --git a/agent/app/service/vm-resource-status.go b/agent/app/service/vm-resource-status.go new file mode 100644 index 000000000..72ad60179 --- /dev/null +++ b/agent/app/service/vm-resource-status.go @@ -0,0 +1,55 @@ +package service + +import ( + "strings" + + "github.com/1Panel-dev/1Panel/agent/constant" +) + +const vmResourceCreating = "creating" + +func vmResourceCreationIncomplete(status string) bool { + return vmResourceStatusEquals(status, vmResourceCreating) || vmResourceStatusEquals(status, constant.StatusFailed) +} + +func normalizeVMResourceStatus(status string) string { + status = strings.TrimSpace(status) + if status == "" { + return constant.StatusUnknown + } + switch strings.ToLower(status) { + case vmResourceCreating: + return vmResourceCreating + case strings.ToLower(constant.StatusFailed): + return constant.StatusFailed + case strings.ToLower(constant.StatusRunning): + return constant.StatusRunning + case strings.ToLower(constant.StatusInactive): + return constant.StatusInactive + case strings.ToLower(constant.StatusDeleted): + return constant.StatusDeleted + case strings.ToLower(constant.StatusUnknown): + return constant.StatusUnknown + case strings.ToLower(constant.StatusBuilding): + return constant.StatusBuilding + case strings.ToLower(constant.StatusDegraded): + return constant.StatusDegraded + case strings.ToLower(constant.StatusInaccessible): + return constant.StatusInaccessible + default: + return status + } +} + +func vmResourceStatusEquals(status string, target string) bool { + return strings.EqualFold(strings.TrimSpace(status), target) +} + +func vmResourceExists(status string) bool { + status = normalizeVMResourceStatus(status) + return !vmResourceStatusEquals(status, constant.StatusDeleted) +} + +func validVMResourceOperateStatus(status string) bool { + return vmResourceStatusEquals(status, constant.StatusRunning) || vmResourceStatusEquals(status, constant.StatusInactive) +} diff --git a/agent/app/service/vm-storage.go b/agent/app/service/vm-storage.go new file mode 100644 index 000000000..ed3909513 --- /dev/null +++ b/agent/app/service/vm-storage.go @@ -0,0 +1,403 @@ +package service + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/vm/types" + "gorm.io/gorm" +) + +func (v *VMService) ListStorages() ([]dto.VMStorageItem, error) { + records, err := vmRepo.ListStorages() + if err != nil { + return nil, err + } + items := v.mergeStorageItems(records) + return items, nil +} + +func (v *VMService) CreateStorage(req dto.VMStorageCreate) error { + ctx, cancel := vmResourceOperationContext(false) + defer cancel() + unlock, err := vmResourceLocks.lock(ctx, "storagePool", req.Name) + if err != nil { + return err + } + defer unlock() + req.Path = strings.TrimSpace(req.Path) + unlockPath, err := vmResourceLocks.lock(ctx, "storagePath", normalizeStoragePath(req.Path)) + if err != nil { + return err + } + defer unlockPath() + itemStorage, err := vmRepo.GetStorage(repo.WithByName(req.Name)) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if itemStorage.ID != 0 { + return buserr.New("ErrRecordExist") + } + records, err := vmRepo.ListStorages() + if err != nil { + return err + } + for _, record := range records { + if sameStoragePath(record.Path, req.Path) { + return buserr.WithName("ErrVMStoragePathInUse", record.Name) + } + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + liveItems, err := v.provider.ListResources(ctx, "storagePool") + if err != nil { + return err + } + for _, live := range liveItems { + if live.Name == req.Name { + return buserr.New("ErrRecordExist") + } + if sameStoragePath(live.Path, req.Path) { + return buserr.WithName("ErrVMStoragePathInUse", live.Name) + } + } + record := model.VMStorage{ + Name: req.Name, Type: req.Type, Path: req.Path, + AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description, + } + if err := vmRepo.CreateStorage(&record); err != nil { + return err + } + if err := v.provider.CreateResource(ctx, types.VMResourceCreate{ + Type: "storagePool", Name: req.Name, SubType: req.Type, Path: req.Path, AutoStart: req.AutoStart, + }); err != nil { + if updateErr := vmRepo.UpdateStorage(record.ID, map[string]interface{}{ + "status": constant.StatusFailed, "error": err.Error(), + }); updateErr != nil { + return errors.Join(err, fmt.Errorf("save storage pool creation failure: %w", updateErr)) + } + return err + } + return vmRepo.UpdateStorage(record.ID, map[string]interface{}{ + "status": constant.StatusRunning, "error": "", + }) +} + +func sameStoragePath(left, right string) bool { + left = normalizeStoragePath(left) + right = normalizeStoragePath(right) + return left != "" && left == right +} + +func normalizeStoragePath(path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + return filepath.Clean(path) +} + +func (v *VMService) SyncStorages() error { + revision, idle := vmResourceLocks.snapshot("storagePool") + if !idle { + return nil + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + liveItems, err := v.provider.ListResources(context.Background(), "storagePool") + if err != nil { + return err + } + return vmResourceLocks.applySnapshot("storagePool", revision, func() error { + records, err := vmRepo.ListStorages() + if err != nil { + return err + } + liveByName := map[string]dto.VMResourceInfo{} + for _, live := range liveItems { + if live.Name == "" { + continue + } + liveByName[live.Name] = live + } + recordsByName := map[string]model.VMStorage{} + for _, record := range records { + if vmResourceStatusEquals(record.Status, vmResourceCreating) { + record.Status = constant.StatusFailed + record.Error = "Storage pool creation was interrupted before its result was saved" + if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{ + "status": record.Status, "error": record.Error, + }); err != nil { + return err + } + } + recordsByName[record.Name] = record + } + for name, live := range liveByName { + vars := map[string]interface{}{ + "type": live.SubType, + "path": live.Path, + "auto_start": live.AutoStart, + "status": normalizeVMResourceStatus(live.Status), + } + if record, ok := recordsByName[name]; ok { + if vmResourceStatusEquals(record.Status, constant.StatusFailed) { + continue + } + if err := vmRepo.UpdateStorage(record.ID, vars); err != nil { + return err + } + continue + } + if err := vmRepo.CreateStorage(&model.VMStorage{ + Name: live.Name, Type: live.SubType, Path: live.Path, + AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), + }); err != nil { + return err + } + } + for _, record := range recordsByName { + if vmResourceStatusEquals(record.Status, constant.StatusFailed) { + continue + } + if _, ok := liveByName[record.Name]; !ok { + if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{ + "status": constant.StatusDeleted, + }); err != nil { + return err + } + } + } + return nil + }) +} + +func (v *VMService) UpdateStorage(req dto.VMStorageUpdate) error { + ctx, cancel := vmResourceOperationContext(false) + defer cancel() + itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID)) + if err != nil { + return err + } + unlock, err := vmResourceLocks.lock(ctx, "storagePool", itemStorage.Name) + if err != nil { + return err + } + defer unlock() + itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID)) + if err != nil { + return err + } + if itemStorage.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + vars := map[string]interface{}{} + if req.Name != "" && req.Name != itemStorage.Name { + return buserr.New("ErrVMStorageNameCannotChange") + } + if req.Path != "" && req.Path != itemStorage.Path { + return buserr.New("ErrVMStoragePathCannotChange") + } + if req.Description != nil { + vars["description"] = *req.Description + } + if req.AutoStart != nil || req.Status != nil { + if vmResourceCreationIncomplete(itemStorage.Status) { + return buserr.WithDetail("ErrInvalidParams", "storage pool creation has not succeeded", nil) + } + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + live, ok, err := v.findVMResourceWithContext(ctx, "storagePool", itemStorage.Name) + if err != nil { + return err + } + if !ok { + _ = vmRepo.UpdateStorage(req.ID, map[string]interface{}{"status": constant.StatusDeleted}) + return buserr.WithDetail("ErrResourceNotExists", "Storage pool does not exist", nil) + } + if live.Status != "" { + vars["status"] = normalizeVMResourceStatus(live.Status) + } + } + if req.AutoStart != nil { + if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{ + Type: "storagePool", Name: itemStorage.Name, AutoStart: req.AutoStart, + }); err != nil { + return err + } + vars["auto_start"] = *req.AutoStart + } + if req.Status != nil { + status := normalizeVMResourceStatus(*req.Status) + if !validVMResourceOperateStatus(status) { + return buserr.WithDetail("ErrInvalidParams", status, nil) + } + if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{ + Type: "storagePool", Name: itemStorage.Name, Status: &status, + }); err != nil { + return err + } + vars["status"] = status + } + if len(vars) == 0 { + return nil + } + return vmRepo.UpdateStorage(req.ID, vars) +} + +func (v *VMService) DeleteStorage(req dto.VMResourceDelete) error { + ctx, cancel := vmResourceOperationContext(req.Force) + defer cancel() + itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return buserr.New("ErrRecordNotFound") + } + return err + } + unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "storagePool", itemStorage.Name, req.Force) + if err != nil { + return err + } + defer unlock() + itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID)) + if err != nil { + return err + } + if len(loadVMResourceRefs("storagePool")[itemStorage.Name]) > 0 { + return buserr.New("ErrVMStorageInUse") + } + if cleanup { + if err := v.deleteVMResource(ctx, "storagePool", itemStorage.Name, req.Force); err != nil { + return err + } + } + return vmRepo.DeleteStorage(repo.WithByID(req.ID)) +} + +func (v *VMService) mergeStorageItems(records []model.VMStorage) []dto.VMStorageItem { + itemsByValue := map[string]dto.VMStorageItem{} + for _, record := range records { + itemsByValue[record.Name] = dto.VMStorageItem{ + ID: record.ID, Name: record.Name, Type: record.Type, + Path: record.Path, AutoStart: record.AutoStart, Description: record.Description, + CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"), + Status: normalizeVMResourceStatus(record.Status), + Error: record.Error, + } + } + if v.helperExists() { + if liveItems, err := v.provider.ListResources(context.Background(), "storagePool"); err == nil { + for _, live := range liveItems { + item, ok := itemsByValue[live.Name] + if !ok || vmResourceCreationIncomplete(item.Status) { + continue + } + item.Type = live.SubType + item.Status = normalizeVMResourceStatus(live.Status) + item.AutoStart = live.AutoStart + item.Capacity = live.Capacity + item.Available = live.Available + itemsByValue[live.Name] = item + } + } + } + vmRefs := loadVMResourceRefs("storagePool") + items := make([]dto.VMStorageItem, 0, len(records)) + for _, record := range records { + item := itemsByValue[record.Name] + if item.Status == "" { + item.Status = constant.StatusDeleted + } + item.VMs = vmRefs[record.Name] + item.VMCount = len(item.VMs) + items = append(items, item) + } + return items +} + +func loadVMResourceRefs(resourceType string) map[string][]dto.VMResourceRef { + records, err := vmRepo.List() + if err != nil { + return map[string][]dto.VMResourceRef{} + } + refs := map[string][]dto.VMResourceRef{} + for _, record := range records { + ref := dto.VMResourceRef{ID: record.ID, Name: record.Name} + switch resourceType { + case "network": + if record.Network != "" { + refs[record.Network] = append(refs[record.Network], ref) + } + case "storagePool": + if record.StoragePool != "" { + refs[record.StoragePool] = append(refs[record.StoragePool], ref) + } + } + } + for name := range refs { + sort.Slice(refs[name], func(i, j int) bool { + return strings.ToLower(refs[name][i].Name) < strings.ToLower(refs[name][j].Name) + }) + } + return refs +} + +func (v *VMService) fillVMStoragePools(items []dto.VMInfo) { + needsPools := false + for _, item := range items { + if item.StoragePool == "" && item.DiskPath != "" { + needsPools = true + break + } + } + if !needsPools { + return + } + pools, err := v.provider.ListResources(context.Background(), "storagePool") + if err != nil { + global.LOG.Warnf("[xpack] resolve VM storage pools failed, retaining metadata: %v", err) + return + } + for i := range items { + if items[i].StoragePool == "" { + items[i].StoragePool = storagePoolForDisk(items[i].DiskPath, pools) + } + } +} + +func storagePoolForDisk(diskPath string, pools []dto.VMResourceInfo) string { + if !filepath.IsAbs(diskPath) { + return "" + } + poolName, bestLength := "", 0 + for _, pool := range pools { + poolPath := normalizeStoragePath(pool.Path) + if pool.Name == "" || !filepath.IsAbs(poolPath) { + continue + } + rel, err := filepath.Rel(poolPath, diskPath) + if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + continue + } + if len(poolPath) > bestLength { + poolName, bestLength = pool.Name, len(poolPath) + } else if len(poolPath) == bestLength && poolName != pool.Name { + poolName = "" + } + } + return poolName +} diff --git a/agent/app/service/vm.go b/agent/app/service/vm.go new file mode 100644 index 000000000..788947d1a --- /dev/null +++ b/agent/app/service/vm.go @@ -0,0 +1,1309 @@ +package service + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/controller" + "github.com/1Panel-dev/1Panel/agent/utils/vm" + "github.com/1Panel-dev/1Panel/agent/utils/vm/protocol" + "github.com/1Panel-dev/1Panel/agent/utils/vm/types" + "github.com/jinzhu/copier" + "github.com/shirou/gopsutil/v4/mem" + "gorm.io/gorm" +) + +type VMService struct { + provider vm.VMProvider + locksMu sync.Mutex + locks map[string]*vmLockEntry +} + +type vmLockEntry struct { + mu sync.Mutex + refs int +} + +// Share the lock across service instances. The metadata written before the +// asynchronous task starts reserves a slot, including while a VM is creating. +var vmCreateMu sync.Mutex + +const ( + defaultVMResourceName = "default" + defaultVMDiskDir = "/var/lib/libvirt/images" + diskBusSATA = "sata" + diskBusVirtIO = "virtio" + osTypeWindows = "windows" + vmMemoryMinMiB = int64(128) + vmDiskMinGiB = int64(1) + vmMemoryReserveRatio = 0.1 + vmMemoryReserveMin = uint64(1024 * 1024 * 1024) + bytePerMiB = uint64(1024 * 1024) + bytePerGiB = uint64(1024 * 1024 * 1024) +) + +var libvirtServiceNames = []string{ + "libvirtd.service", + "virtqemud.service", + "virtnetworkd.service", + "libvirt-bin.service", + "libvirtd", + "virtqemud", + "virtnetworkd", + "libvirt-bin", + "libvirt", +} + +type IVMService interface { + CheckEnvironment() (dto.VMEnvironment, error) + HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) + EnableEnvironment() error + SyncResourcesOnStartup() error + SyncVMs() error + Search(req dto.SearchWithPage) (int64, interface{}, error) + Get(name string, checkDisks bool) (dto.VMInfo, error) + GetStats(name string) (dto.VMStats, error) + Create(req dto.VMCreate, limit int) (dto.VMTaskResp, error) + ListTemplates() ([]dto.VMTemplateInfo, error) + CreateTemplate(req dto.VMTemplateCreate) (dto.VMTaskResp, error) + DeleteTemplate(req dto.VMTemplateDelete) (dto.VMTaskResp, error) + Update(req dto.VMUpdate) (dto.VMTaskResp, error) + ResizeDisk(req dto.VMDiskResize) (dto.VMTaskResp, error) + Rename(req dto.VMRename) (dto.VMTaskResp, error) + Delete(req dto.VMDelete) (dto.VMTaskResp, error) + ListSnapshots(req dto.VMSnapshotSearch) ([]dto.VMSnapshotInfo, error) + CreateSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) + RecoverSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) + DeleteSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) + ScanOrphans() ([]dto.VMOrphanItem, error) + CleanOrphans(req dto.VMOrphanClean) ([]dto.VMOrphanItem, error) + Operate(req dto.VMOperate) error + Console(name string) (*dto.VMGraphics, error) + + SearchIso(req dto.SearchPageWithType) (int64, interface{}, error) + ListIso() ([]dto.VMIsoInfo, error) + CreateIso(req dto.VMIsoCreate) error + UpdateIso(req dto.VMIsoUpdate) error + DeleteIso(req dto.VMIsoDelete) error + StartIsoImport(req dto.VMIsoImportStart) (uint, error) + UpdateIsoImport(req dto.VMIsoImportUpdate) error + + ListNetworks() ([]dto.VMNetworkItem, error) + ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) + ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) + CreateNetwork(req dto.VMNetworkCreate) error + SyncNetworks() error + UpdateNetwork(req dto.VMNetworkUpdate) error + DeleteNetwork(req dto.VMResourceDelete) error + + ListStorages() ([]dto.VMStorageItem, error) + CreateStorage(req dto.VMStorageCreate) error + SyncStorages() error + UpdateStorage(req dto.VMStorageUpdate) error + DeleteStorage(req dto.VMResourceDelete) error +} + +func (v *VMService) ScanOrphans() ([]dto.VMOrphanItem, error) { + paths, err := protectedVMPaths(0) + if err != nil { + return nil, err + } + return v.provider.ScanOrphans(context.Background(), paths) +} + +func (v *VMService) CleanOrphans(req dto.VMOrphanClean) ([]dto.VMOrphanItem, error) { + paths, err := protectedVMPaths(0) + if err != nil { + return nil, err + } + return v.provider.CleanOrphans(context.Background(), req.Items, paths) +} + +func protectedVMPaths(excludeVMID uint) ([]string, error) { + vms, err := vmRepo.List() + if err != nil { + return nil, err + } + templates, err := vmRepo.ListTemplates() + if err != nil { + return nil, err + } + paths := make([]string, 0, 2*len(vms)+len(templates)) + for _, item := range vms { + if item.ID == excludeVMID { + continue + } + if item.DiskPath != "" { + paths = append(paths, item.DiskPath) + } + if item.ImagePath != "" { + paths = append(paths, item.ImagePath) + } + } + for _, item := range templates { + if item.DiskPath != "" { + paths = append(paths, item.DiskPath) + } + } + return paths, nil +} + +func NewIVMService() IVMService { + provider, _ := vm.NewVMProvider() + return &VMService{provider: provider, locks: make(map[string]*vmLockEntry)} +} + +func (v *VMService) CheckEnvironment() (dto.VMEnvironment, error) { + arch := runtime.GOARCH + env := dto.VMEnvironment{ + Arch: arch, + ArchSupported: arch == "amd64" || arch == "arm64", + } + if !env.ArchSupported { + return env, nil + } + helperExist := v.provider.HelperStatus() + env.HelperExist = helperExist + if !helperExist { + env.HelperMessage = buserr.New("ErrVMHelperNotFound").Error() + } + helperReady := helperExist + if helperExist { + checkItems, checkErr := v.provider.CheckEnvironment(context.Background()) + if checkErr != nil { + helperReady = false + env.HelperMessage = checkErr.Error() + } + for _, item := range checkItems { + switch item.Name { + case "kvm": + env.KVMExist = item.IsExist + env.KVMMessage = item.Message + case "storage": + env.StorageExist = item.IsExist + env.StorageMessage = item.Message + case "libvirt": + env.LibvirtExist = item.IsExist + env.LibvirtActive = item.Passed || item.IsActive + env.LibvirtMessage = item.Message + } + } + } + if libvirtExist, _, _ := v.loadLibvirtServiceStatus(); libvirtExist { + env.LibvirtExist = true + } + env.Ready = helperReady && env.KVMExist && env.StorageExist && env.LibvirtActive + return env, nil +} + +func (v *VMService) HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) { + if !v.helperExists() { + return dto.VMHealthCheckResult{}, buserr.New("ErrVMHelperNotFound") + } + return v.provider.HealthCheck(ctx, name) +} + +func (v *VMService) EnableEnvironment() error { + client, err := controller.New() + if err != nil { + return err + } + found := false + for _, service := range libvirtServiceNames { + exist, _ := client.IsExist(service) + if !exist { + continue + } + found = true + if err := client.Operate("start", service); err != nil { + return err + } + } + if !found { + return buserr.New("ErrRecordNotFound") + } + return nil +} + +func (v *VMService) loadLibvirtServiceStatus() (bool, bool, bool) { + client, err := controller.New() + if err != nil { + return false, false, false + } + isExist := false + isEnable := false + isActive := false + for _, service := range libvirtServiceNames { + exist, _ := client.IsExist(service) + if !exist { + continue + } + isExist = true + if enable, _ := client.IsEnable(service); enable { + isEnable = true + } + if active, _ := client.IsActive(service); active { + isActive = true + } + if isEnable && isActive { + break + } + } + return isExist, isEnable, isActive +} + +func (v *VMService) SyncResourcesOnStartup() error { + env, err := v.CheckEnvironment() + if err != nil { + return err + } + if !env.HelperExist { + global.LOG.Info("skip VM resource sync on startup: 1panel-kvm not found") + return nil + } + if !env.KVMExist { + global.LOG.Info("skip VM resource sync on startup: kvm not found") + return nil + } + if !env.LibvirtActive { + global.LOG.Info("skip VM resource sync on startup: libvirt is not active") + return nil + } + if err := v.SyncNetworks(); err != nil { + return fmt.Errorf("sync VM networks failed: %w", err) + } + if err := v.SyncStorages(); err != nil { + return fmt.Errorf("sync VM storage pools failed: %w", err) + } + if err := v.SyncVMs(); err != nil { + return fmt.Errorf("sync virtual machines failed: %w", err) + } + global.LOG.Info("VM networks, storage pools, and virtual machines synced on startup") + return nil +} + +// SyncVMs imports libvirt domains into the panel metadata and refreshes metadata +// for already registered VMs. Metadata maintained only by the panel, such as the +// description and image path, is intentionally retained. +func (v *VMService) SyncVMs() error { + vmCreateMu.Lock() + defer vmCreateMu.Unlock() + + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + liveItems, err := v.provider.List(context.Background()) + if err != nil { + return err + } + v.fillVMStoragePools(liveItems) + records, err := vmRepo.List() + if err != nil { + return err + } + recordsByName := make(map[string]model.VirtualMachine, len(records)) + for _, record := range records { + recordsByName[record.Name] = record + } + liveByName := make(map[string]struct{}, len(liveItems)) + for _, live := range liveItems { + if live.Name == "" { + continue + } + liveByName[live.Name] = struct{}{} + if record, ok := recordsByName[live.Name]; ok { + if err := vmRepo.Update(record.ID, vmSyncMetadataMap(live)); err != nil { + return err + } + continue + } + if err := vmRepo.Create(&model.VirtualMachine{ + Name: live.Name, UUID: live.UUID, CPU: live.CPU, Memory: live.Memory, + DiskSize: live.DiskSize, DiskBus: defaultDiskBus(live.DiskBus), DiskPath: live.DiskPath, StoragePool: live.StoragePool, + ISOPaths: live.ISOPaths, Network: live.Network, BootOrder: live.BootOrder, AutoStart: live.AutoStart, Status: live.Status, + }); err != nil { + return err + } + } + for _, record := range records { + if _, ok := liveByName[record.Name]; ok || record.UUID == "" { + continue + } + if err := vmRepo.Update(record.ID, map[string]interface{}{"status": strings.ToLower(constant.StatusDeleted)}); err != nil { + return err + } + } + return nil +} + +func vmSyncMetadataMap(item dto.VMInfo) map[string]interface{} { + vars := map[string]interface{}{ + "uuid": item.UUID, "cpu": item.CPU, "memory": item.Memory, "disk_bus": defaultDiskBus(item.DiskBus), + "disk_path": item.DiskPath, "iso_paths": item.ISOPaths, + "network": item.Network, "boot_order": item.BootOrder, "auto_start": item.AutoStart, "status": item.Status, + } + if item.StoragePool != "" { + vars["storage_pool"] = item.StoragePool + } + if item.DiskSize > 0 { + vars["disk_size"] = item.DiskSize + } + return vars +} + +func (v *VMService) Search(req dto.SearchWithPage) (int64, interface{}, error) { + options := []repo.DBOption{repo.WithOrderDesc("created_at")} + if req.Info != "" { + options = append(options, repo.WithByName(req.Info)) + } + total, records, err := vmRepo.Page(req.Page, req.PageSize, options...) + if err != nil { + return 0, nil, err + } + var domains []dto.VMInfo + if v.helperExists() { + domains, err = v.provider.List(context.Background()) + if err != nil { + global.LOG.Warnf("[xpack] list live VM domains failed, fallback to metadata: %v", err) + } + } + domainByName := make(map[string]dto.VMInfo, len(domains)) + for _, domain := range domains { + domainByName[domain.Name] = domain + } + list := make([]dto.VMInfo, 0, len(records)) + for _, record := range records { + if domain, ok := domainByName[record.Name]; ok { + mergeVMMetadata(&domain, record) + list = append(list, domain) + continue + } + var item dto.VMInfo + if err := copier.Copy(&item, &record); err != nil { + return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) + } + v.fillMetadataVMStatus(&item) + list = append(list, item) + } + return total, list, nil +} + +func (v *VMService) Get(name string, checkDisks bool) (dto.VMInfo, error) { + if !v.helperExists() { + return dto.VMInfo{}, buserr.New("ErrVMHelperNotFound") + } + item, err := v.provider.Get(context.Background(), name, checkDisks) + if err != nil { + return dto.VMInfo{}, err + } + if record, err := vmRepo.Get(repo.WithByName(name)); err == nil { + mergeVMMetadata(&item, record) + } + return item, nil +} + +func (v *VMService) GetStats(name string) (dto.VMStats, error) { + if !v.helperExists() { + return dto.VMStats{}, buserr.New("ErrVMHelperNotFound") + } + return v.provider.Stats(context.Background(), name) +} + +func (v *VMService) Create(req dto.VMCreate, limit int) (dto.VMTaskResp, error) { + vmCreateMu.Lock() + defer vmCreateMu.Unlock() + + if limit > 0 { + count, err := vmRepo.Count() + if err != nil { + return dto.VMTaskResp{}, err + } + if count >= int64(limit) { + return dto.VMTaskResp{}, buserr.WithMap("ErrVMCountLimit", map[string]interface{}{"limit": limit}, nil) + } + } + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + if _, err := vmRepo.Get(repo.WithByName(req.Name)); err == nil { + return dto.VMTaskResp{}, buserr.New("ErrRecordExist") + } else if err != gorm.ErrRecordNotFound { + return dto.VMTaskResp{}, err + } + if err := v.validateMemoryLimit(req.Memory, ""); err != nil { + return dto.VMTaskResp{}, err + } + if req.TemplateID == 0 && len(req.ISOPaths) == 0 { + return dto.VMTaskResp{}, buserr.WithDetail("ErrVMInvalidInput", "ISO path is required", nil) + } + cfg := types.VMConfig{ + Name: req.Name, CPU: req.CPU, MemoryMiB: req.Memory, DiskSizeGiB: req.DiskSize, + DiskBus: resolveDiskBus(req.OSType, req.DiskBus), DiskPath: req.DiskPath, StoragePool: req.StoragePool, ISOPaths: req.ISOPaths, + ImagePath: req.ImagePath, Network: req.Network, BootOrder: req.BootOrder, AutoStart: req.AutoStart, + StartAfterCreate: req.StartAfterCreate, + } + cfg.BootOrder = "hd,cdrom" + if req.TemplateID != 0 { + template, err := vmRepo.GetTemplate(repo.WithByID(req.TemplateID)) + if err != nil { + return dto.VMTaskResp{}, err + } + if _, statErr := os.Stat(template.DiskPath); statErr != nil { + return dto.VMTaskResp{}, statErr + } + cfg.ImagePath = template.DiskPath + cfg.DiskBus = defaultDiskBus(template.DiskBus) + cfg.ISOPaths = []string{} + cfg.BootOrder = "hd" + if template.DiskSize > 0 && cfg.DiskSizeGiB < template.DiskSize { + return dto.VMTaskResp{}, buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size must be at least %d GiB", template.DiskSize), nil) + } + if cfg.DiskSizeGiB == 0 { + cfg.DiskSizeGiB = 1 + } + if cfg.StoragePool == "" { + cfg.StoragePool = template.StoragePool + } + } + if err := v.validateDiskLimit(cfg.DiskSizeGiB, cfg.StoragePool); err != nil { + return dto.VMTaskResp{}, err + } + createMetadataReq := v.buildCreateMetadataReq(req, cfg) + if createMetadataReq.DiskPath != "" { + if _, err := os.Lstat(createMetadataReq.DiskPath); err == nil { + return dto.VMTaskResp{}, buserr.WithDetail("ErrVMImageAlreadyExists", createMetadataReq.DiskPath, nil) + } else if !os.IsNotExist(err) { + return dto.VMTaskResp{}, err + } + } + createTaskKey := "VMCreateTask" + if req.TemplateID != 0 { + createTaskKey = "VMCreateFromTemplateTask" + } + taskName := vmTaskName(createTaskKey, req.Name) + taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeVm, req.TaskID, 0) + if err != nil { + return dto.VMTaskResp{}, err + } + if err := v.saveMetadata(createMetadataReq, dto.VMInfo{DiskPath: createMetadataReq.DiskPath}, taskItem.Task.ID); err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + var domain dto.VMInfo + var err error + if req.TemplateID != 0 { + domain, err = v.provider.CreateFromTemplate(context.Background(), cfg) + } else { + domain, err = v.provider.Create(context.Background(), cfg) + } + if err != nil { + return err + } + v.syncCreatedDefaultResourceMetadata(cfg) + return v.saveMetadata(createMetadataReq, domain, t.Task.ID) + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) ListTemplates() ([]dto.VMTemplateInfo, error) { + records, err := vmRepo.ListTemplates() + if err != nil { + return nil, err + } + items := make([]dto.VMTemplateInfo, 0, len(records)) + for _, record := range records { + item := dto.VMTemplateInfo{ + ID: record.ID, Name: record.Name, SourceVM: record.SourceVM, CPU: record.CPU, Memory: record.Memory, + DiskSize: record.DiskSize, DiskBus: defaultDiskBus(record.DiskBus), DiskPath: record.DiskPath, StoragePool: record.StoragePool, + Network: record.Network, BootOrder: record.BootOrder, CreatedAt: record.CreatedAt, Description: record.Description, + } + if stat, statErr := os.Stat(record.DiskPath); statErr == nil { + item.IsExist = true + item.Size = stat.Size() + } + items = append(items, item) + } + return items, nil +} + +func (v *VMService) CreateTemplate(req dto.VMTemplateCreate) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + if item, _ := vmRepo.GetTemplate(repo.WithByName(req.TemplateName)); item.ID != 0 { + return dto.VMTaskResp{}, buserr.New("ErrRecordExist") + } + source, err := v.Get(req.Name, false) + if err != nil { + return dto.VMTaskResp{}, err + } + storagePool := req.StoragePool + if storagePool == "" { + storagePool = source.StoragePool + } + if storagePool == "" { + storagePool = "default" + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskName := vmTemplateTaskName("VMTemplateCreateTask", req.Name, req.TemplateName) + taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + tpl, err := v.provider.CreateTemplate(context.Background(), types.VMTemplateCreate{ + Name: req.TemplateName, SourceName: req.Name, StoragePool: storagePool, + }) + if err != nil { + return err + } + templateRecord := &model.VMTemplate{ + Name: req.TemplateName, SourceVM: req.Name, CPU: source.CPU, Memory: source.Memory, + DiskSize: tpl.DiskSize, DiskBus: defaultDiskBus(source.DiskBus), DiskPath: tpl.DiskPath, StoragePool: storagePool, + Network: source.Network, BootOrder: source.BootOrder, Description: req.Description, + } + if err := vmRepo.CreateTemplate(templateRecord); err != nil { + if tpl.DiskPath != "" { + _ = os.Remove(tpl.DiskPath) + } + return err + } + return nil + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) DeleteTemplate(req dto.VMTemplateDelete) (dto.VMTaskResp, error) { + record, err := vmRepo.GetTemplate(repo.WithByID(req.ID)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskName := vmTaskName("VMTemplateDeleteTask", record.Name) + taskItem, err := task.NewTask(taskName, task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + if req.DeleteDisk && record.DiskPath != "" { + if err := os.Remove(record.DiskPath); err != nil && !os.IsNotExist(err) { + return err + } + } + return vmRepo.DeleteTemplate(repo.WithByID(record.ID)) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) Update(req dto.VMUpdate) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + if err := v.validateMemoryLimit(req.Memory, req.Name); err != nil { + return dto.VMTaskResp{}, err + } + cfg := types.VMConfig{ + Name: req.Name, CPU: req.CPU, MemoryMiB: req.Memory, ISOPaths: req.ISOPaths, + Network: req.Network, BootOrder: req.BootOrder, AutoStart: req.AutoStart, + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskName := vmTaskName("VMUpdateTask", req.Name) + taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + domain, err := v.provider.Update(context.Background(), cfg) + if err != nil { + return err + } + return v.updateMetadata(req, domain, t.Task.ID) + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) ResizeDisk(req dto.VMDiskResize) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskName := i18n.GetMsgWithMap("VMDiskResizeTask", map[string]interface{}{"name": req.Name, "disk": req.Target}) + taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + domain, err := v.provider.ResizeDisk(context.Background(), req) + if err != nil { + return err + } + vars := map[string]interface{}{"disk_path": domain.DiskPath, "task_id": t.Task.ID} + if domain.DiskSize > 0 { + vars["disk_size"] = domain.DiskSize + } + return vmRepo.Update(record.ID, vars) + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) Rename(req dto.VMRename) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + if req.Name == req.NewName { + return dto.VMTaskResp{}, buserr.New("ErrRecordExist") + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + if item, _ := vmRepo.Get(repo.WithByName(req.NewName)); item.ID != 0 { + return dto.VMTaskResp{}, buserr.New("ErrRecordExist") + } + taskName := vmRenameTaskName("VMRenameTask", req.Name, req.NewName) + taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMRenameLock(req.Name, req.NewName, func() error { + domain, err := v.provider.Rename(context.Background(), req.Name, req.NewName) + if err != nil { + return err + } + if err := v.renameMetadata(record, req, domain, t.Task.ID); err != nil { + _, _ = v.provider.Rename(context.Background(), req.NewName, req.Name) + return err + } + return nil + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) Delete(req dto.VMDelete) (dto.VMTaskResp, error) { + record, err := vmRepo.Get(repo.WithByID(req.ID)) + if err != nil { + return dto.VMTaskResp{}, err + } + if !v.helperExists() && !req.Force { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + taskName := vmTaskName("VMDeleteTask", record.Name) + taskItem, err := task.NewTask(taskName, task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { + return v.withVMLock(record.Name, func() error { + if !v.helperExists() { + if !req.Force { + return buserr.New("ErrVMHelperNotFound") + } + t.Log(i18n.GetMsgWithMap("VMForceDeleteMetadataOnly", map[string]interface{}{"name": record.Name})) + } else { + paths, err := protectedVMPaths(record.ID) + if err != nil { + return err + } + if record.ImagePath != "" { + paths = append(paths, record.ImagePath) + } + if err := v.provider.Delete(context.Background(), record.Name, req.Force, true, paths); err != nil { + var helperErr *protocol.Error + if errors.As(err, &helperErr) && helperErr.Code == "MAIN_DISK_DELETE_FAILED" { + return buserr.WithErr("ErrVMCommandFailed", err) + } + if !req.Force { + return err + } + t.Log(i18n.GetMsgWithMap("VMForceDeleteMetadataAfterLibvirtFailed", map[string]interface{}{"name": record.Name, "err": err})) + } + } + if record.ID != 0 { + return vmRepo.Delete(repo.WithByID(record.ID)) + } + return nil + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) ListSnapshots(req dto.VMSnapshotSearch) ([]dto.VMSnapshotInfo, error) { + if !v.helperExists() { + return nil, buserr.New("ErrVMHelperNotFound") + } + items, err := v.provider.ListSnapshots(context.Background(), req.Name) + if err != nil { + return nil, err + } + sortVMSnapshotItems(items) + return items, nil +} + +func sortVMSnapshotItems(items []dto.VMSnapshotInfo) { + sort.SliceStable(items, func(i, j int) bool { + if items[i].CreatedAt != items[j].CreatedAt { + return items[i].CreatedAt > items[j].CreatedAt + } + return items[i].Name > items[j].Name + }) +} + +func (v *VMService) CreateSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotCreateTask", req), task.TaskCreate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotCreateStep", req), func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + return v.provider.CreateSnapshot(context.Background(), req.Name, req.SnapshotName, req.Description) + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) RecoverSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotRecoverTask", req), task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotRecoverStep", req), func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + if err := v.provider.RecoverSnapshot(context.Background(), req.Name, req.SnapshotName); err != nil { + return err + } + domain, err := v.provider.Get(context.Background(), req.Name, false) + if err != nil { + return buserr.WithErr("ErrVMSnapshotMetadataSync", err) + } + if domain.DiskSize <= 0 { + return buserr.New("ErrVMSnapshotMetadataSync") + } + if err := vmRepo.Update(record.ID, map[string]interface{}{"disk_path": domain.DiskPath, "disk_size": domain.DiskSize}); err != nil { + return buserr.WithErr("ErrVMSnapshotMetadataSync", err) + } + return nil + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func (v *VMService) DeleteSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { + if !v.helperExists() { + return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") + } + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotDeleteTask", req), task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) + if err != nil { + return dto.VMTaskResp{}, err + } + taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotDeleteStep", req), func(t *task.Task) error { + return v.withVMLock(req.Name, func() error { + return v.provider.DeleteSnapshot(context.Background(), req.Name, req.SnapshotName) + }) + }, nil, 0, 30*time.Minute) + go taskItem.Execute() + return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil +} + +func vmSnapshotTaskName(key string, req dto.VMSnapshotReq) string { + return i18n.GetMsgWithMap(key, map[string]interface{}{ + "vm": req.Name, + "snapshot": req.SnapshotName, + }) +} + +func vmTaskName(key, name string) string { + return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name}) +} + +func vmRenameTaskName(key, name, newName string) string { + return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name, "newName": newName}) +} + +func vmTemplateTaskName(key, name, template string) string { + return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name, "template": template}) +} + +func (v *VMService) Operate(req dto.VMOperate) error { + switch req.Operate { + case "start": + return v.lifecycle(req.Name, v.provider.Start) + case "shutdown": + return v.lifecycle(req.Name, v.provider.Shutdown) + case "reboot": + return v.lifecycle(req.Name, v.provider.Reboot) + case "destroy": + return v.lifecycle(req.Name, v.provider.Destroy) + case "suspend": + return v.lifecycle(req.Name, v.provider.Suspend) + case "resume": + return v.lifecycle(req.Name, v.provider.Resume) + default: + return buserr.WithName("ErrNotSupportType", req.Operate) + } +} + +func (v *VMService) Console(name string) (*dto.VMGraphics, error) { + if !v.helperExists() { + return nil, buserr.New("ErrVMHelperNotFound") + } + return v.provider.Console(context.Background(), name) +} + +func (v *VMService) lifecycle(name string, fn func(context.Context, string) error) error { + if !v.helperExists() { + return buserr.New("ErrVMHelperNotFound") + } + return v.withVMLock(name, func() error { + return fn(context.Background(), name) + }) +} + +func (v *VMService) helperExists() bool { + return v.provider.HelperStatus() +} + +func (v *VMService) withVMLock(name string, fn func() error) error { + lock := v.acquireVMLock(name) + defer v.releaseVMLock(name, lock) + return fn() +} + +func (v *VMService) acquireVMLock(name string) *vmLockEntry { + v.locksMu.Lock() + if v.locks == nil { + v.locks = make(map[string]*vmLockEntry) + } + lock := v.locks[name] + if lock == nil { + lock = &vmLockEntry{} + v.locks[name] = lock + } + lock.refs++ + v.locksMu.Unlock() + lock.mu.Lock() + return lock +} + +func (v *VMService) releaseVMLock(name string, lock *vmLockEntry) { + lock.mu.Unlock() + v.locksMu.Lock() + lock.refs-- + if lock.refs == 0 && v.locks[name] == lock { + delete(v.locks, name) + } + v.locksMu.Unlock() +} + +func (v *VMService) withVMRenameLock(name string, newName string, fn func() error) error { + if name > newName { + name, newName = newName, name + } + return v.withVMLock(name, func() error { + return v.withVMLock(newName, fn) + }) +} + +func (v *VMService) saveMetadata(req dto.VMCreate, domain dto.VMInfo, taskID string) error { + record, err := vmRepo.Get(repo.WithByName(req.Name)) + diskBus := defaultDiskBus(req.DiskBus) + if domain.DiskBus != "" { + diskBus = domain.DiskBus + } + if domain.UUID != "" { + req.ISOPaths = domain.ISOPaths + } + data := &model.VirtualMachine{ + Name: req.Name, UUID: domain.UUID, CPU: req.CPU, Memory: req.Memory, DiskSize: req.DiskSize, + DiskBus: diskBus, DiskPath: domain.DiskPath, StoragePool: req.StoragePool, ISOPaths: req.ISOPaths, + ImagePath: req.ImagePath, Network: req.Network, BootOrder: req.BootOrder, + AutoStart: req.AutoStart, TaskID: taskID, CreateTaskID: taskID, Description: req.Description, + } + if err == nil && record.ID != 0 { + return vmRepo.Update(record.ID, metadataUpdateMap(*data)) + } + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + return vmRepo.Create(data) +} + +func (v *VMService) buildCreateMetadataReq(req dto.VMCreate, cfg types.VMConfig) dto.VMCreate { + out := req + out.DiskSize = cfg.DiskSizeGiB + out.DiskBus = cfg.DiskBus + out.DiskPath = cfg.DiskPath + out.StoragePool = cfg.StoragePool + out.ISOPaths = cfg.ISOPaths + out.BootOrder = cfg.BootOrder + if out.StoragePool == "" { + out.StoragePool = defaultVMResourceName + } + if req.TemplateID != 0 { + out.ImagePath = "" + } + if out.DiskPath == "" { + out.DiskPath = v.defaultCreateDiskPath(out.Name, out.StoragePool) + } + return out +} + +func (v *VMService) defaultCreateDiskPath(name string, storagePool string) string { + if storagePool == "" { + storagePool = defaultVMResourceName + } + if live, ok, err := v.findVMResource("storagePool", storagePool); err == nil && ok && live.Path != "" { + return filepath.Join(live.Path, name+".qcow2") + } + if storagePool == defaultVMResourceName { + return filepath.Join(defaultVMDiskDir, name+".qcow2") + } + return "" +} + +func resolveDiskBus(osType string, diskBus string) string { + if diskBus != "" { + return diskBus + } + if osType == osTypeWindows { + return diskBusSATA + } + return diskBusVirtIO +} + +func defaultDiskBus(diskBus string) string { + if diskBus == "" { + return diskBusVirtIO + } + return diskBus +} + +func (v *VMService) validateMemoryLimit(memoryMiB int64, currentName string) error { + if memoryMiB < vmMemoryMinMiB { + return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("memory must be at least %d MiB", vmMemoryMinMiB), nil) + } + memoryInfo, err := mem.VirtualMemory() + if err != nil || memoryInfo.Total == 0 { + return buserr.WithDetail("ErrVMInvalidInput", "failed to load host memory limit", err) + } + allocatedBytes, currentBytes, err := v.loadAllocatedVMMemory(currentName) + if err != nil { + return err + } + reservedBytes := uint64(float64(memoryInfo.Total) * vmMemoryReserveRatio) + if reservedBytes < vmMemoryReserveMin { + reservedBytes = vmMemoryReserveMin + } + if reservedBytes >= memoryInfo.Total { + return buserr.WithDetail("ErrVMInvalidInput", "host memory is insufficient", nil) + } + availableBytes := memoryInfo.Total - reservedBytes + if allocatedBytes > currentBytes { + usedByOthers := allocatedBytes - currentBytes + if usedByOthers >= availableBytes { + return buserr.WithDetail("ErrVMInvalidInput", "host memory is insufficient", nil) + } + availableBytes -= usedByOthers + } + requestBytes := uint64(memoryMiB) * bytePerMiB + if requestBytes > availableBytes { + return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("memory exceeds available limit: %d MiB", availableBytes/bytePerMiB), nil) + } + return nil +} + +func (v *VMService) loadAllocatedVMMemory(currentName string) (uint64, uint64, error) { + domains, err := v.provider.List(context.Background()) + if err != nil { + return 0, 0, buserr.WithDetail("ErrVMInvalidInput", "failed to load virtual machine memory usage", err) + } + var allocated uint64 + var current uint64 + for _, domain := range domains { + if !vmMemoryAllocatedStatus(domain.Status) { + continue + } + memoryBytes := uint64(domain.Memory) * bytePerMiB + allocated += memoryBytes + if currentName != "" && domain.Name == currentName { + current = memoryBytes + } + } + return allocated, current, nil +} + +func vmMemoryAllocatedStatus(status string) bool { + switch strings.ToLower(status) { + case "running", "blocked", "paused", "shutdown", "pmsuspended": + return true + default: + return false + } +} + +func (v *VMService) validateDiskLimit(diskSizeGiB int64, storagePool string) error { + if diskSizeGiB < vmDiskMinGiB { + return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size must be at least %d GiB", vmDiskMinGiB), nil) + } + if storagePool == "" { + storagePool = defaultVMResourceName + } + live, ok, err := v.findVMResource("storagePool", storagePool) + if err != nil { + return err + } + if !ok || live.Available == 0 { + return nil + } + if uint64(diskSizeGiB)*bytePerGiB > live.Available { + return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size exceeds storage pool available space: %d GiB", live.Available/bytePerGiB), nil) + } + return nil +} + +func (v *VMService) syncCreatedDefaultResourceMetadata(cfg types.VMConfig) { + if cfg.Network == defaultVMResourceName { + if err := v.upsertNetworkMetadata(defaultVMResourceName); err != nil { + global.LOG.Warnf("[xpack] sync default VM network metadata failed: %v", err) + } + } + storagePool := cfg.StoragePool + if storagePool == "" { + storagePool = defaultVMResourceName + } + if storagePool == defaultVMResourceName { + if err := v.upsertStorageMetadata(defaultVMResourceName); err != nil { + global.LOG.Warnf("[xpack] sync default VM storage metadata failed: %v", err) + } + } +} + +func (v *VMService) upsertNetworkMetadata(name string) error { + live, ok, err := v.findVMResource("network", name) + if err != nil || !ok { + return err + } + vars := map[string]interface{}{ + "type": live.SubType, + "bridge_name": live.BridgeName, + "source_interface": live.SourceInterface, + "cidr": live.CIDR, + "gateway": live.Gateway, + "dhcp_start": live.DHCPStart, + "dhcp_end": live.DHCPEnd, + "auto_start": live.AutoStart, + "status": normalizeVMResourceStatus(live.Status), + } + record, err := vmRepo.GetNetwork(repo.WithByName(name)) + if err == nil && record.ID != 0 { + return vmRepo.UpdateNetwork(record.ID, vars) + } + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + return vmRepo.CreateNetwork(&model.VMNetwork{ + Name: live.Name, Type: live.SubType, BridgeName: live.BridgeName, SourceInterface: live.SourceInterface, + CIDR: live.CIDR, Gateway: live.Gateway, DHCPStart: live.DHCPStart, DHCPEnd: live.DHCPEnd, + AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), + }) +} + +func (v *VMService) upsertStorageMetadata(name string) error { + live, ok, err := v.findVMResource("storagePool", name) + if err != nil || !ok { + return err + } + vars := map[string]interface{}{ + "type": live.SubType, + "path": live.Path, + "auto_start": live.AutoStart, + "status": normalizeVMResourceStatus(live.Status), + } + record, err := vmRepo.GetStorage(repo.WithByName(name)) + if err == nil && record.ID != 0 { + return vmRepo.UpdateStorage(record.ID, vars) + } + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + return vmRepo.CreateStorage(&model.VMStorage{ + Name: live.Name, Type: live.SubType, Path: live.Path, + AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), + }) +} + +func (v *VMService) findVMResource(resourceType, name string) (dto.VMResourceInfo, bool, error) { + return v.findVMResourceWithContext(context.Background(), resourceType, name) +} + +func (v *VMService) findVMResourceWithContext(ctx context.Context, resourceType, name string) (dto.VMResourceInfo, bool, error) { + liveItems, err := v.provider.ListResources(ctx, resourceType) + if err != nil { + return dto.VMResourceInfo{}, false, err + } + for _, live := range liveItems { + if live.Name == name { + return live, true, nil + } + } + return dto.VMResourceInfo{}, false, nil +} + +func (v *VMService) updateMetadata(req dto.VMUpdate, domain dto.VMInfo, taskID string) error { + record, err := vmRepo.Get(repo.WithByName(req.Name)) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return err + } + return vmRepo.Update(record.ID, map[string]interface{}{ + "uuid": domain.UUID, "cpu": req.CPU, "memory": req.Memory, "iso_paths": domain.ISOPaths, + "network": req.Network, "boot_order": domain.BootOrder, "auto_start": req.AutoStart, + "description": req.Description, "task_id": taskID, + }) +} + +func (v *VMService) renameMetadata(record model.VirtualMachine, req dto.VMRename, domain dto.VMInfo, taskID string) error { + vars := metadataUpdateMap(record) + vars["name"] = req.NewName + vars["uuid"] = domain.UUID + vars["cpu"] = domain.CPU + vars["memory"] = domain.Memory + if domain.DiskSize > 0 { + vars["disk_size"] = domain.DiskSize + } + vars["disk_path"] = domain.DiskPath + vars["iso_paths"] = domain.ISOPaths + vars["network"] = domain.Network + vars["boot_order"] = domain.BootOrder + vars["auto_start"] = domain.AutoStart + vars["task_id"] = taskID + if err := vmRepo.Update(record.ID, vars); err != nil { + return err + } + if err := vmRepo.UpdateTemplateSource(req.Name, req.NewName); err != nil { + rollbackVars := metadataUpdateMap(record) + rollbackVars["name"] = req.Name + _ = vmRepo.Update(record.ID, rollbackVars) + return err + } + return nil +} + +func metadataUpdateMap(vm model.VirtualMachine) map[string]interface{} { + return map[string]interface{}{ + "uuid": vm.UUID, "cpu": vm.CPU, "memory": vm.Memory, "disk_size": vm.DiskSize, "disk_bus": vm.DiskBus, + "disk_path": vm.DiskPath, "storage_pool": vm.StoragePool, "iso_paths": vm.ISOPaths, + "image_path": vm.ImagePath, "network": vm.Network, "boot_order": vm.BootOrder, + "auto_start": vm.AutoStart, "task_id": vm.TaskID, "create_task_id": vm.CreateTaskID, "description": vm.Description, + } +} + +func mergeVMMetadata(item *dto.VMInfo, record model.VirtualMachine) { + item.ID = record.ID + item.StoragePool = record.StoragePool + item.ImagePath = record.ImagePath + if item.DiskPath == "" { + item.DiskPath = record.DiskPath + } + if item.DiskBus == "" { + item.DiskBus = defaultDiskBus(record.DiskBus) + } + item.AutoStart = record.AutoStart + item.TaskID = record.TaskID + item.CreateTaskID = record.CreateTaskID + item.Description = record.Description + if item.UUID == "" { + item.UUID = record.UUID + } + if item.DiskSize == 0 { + item.DiskSize = record.DiskSize + } + if item.Status == "" { + item.Status = record.Status + } +} + +func (v *VMService) fillMetadataVMStatus(item *dto.VMInfo) { + if item.Status != "" { + return + } + taskID := item.CreateTaskID + if taskID == "" { + taskID = item.TaskID + } + if taskID == "" { + return + } + taskRecord, err := taskRepo.GetFirst(taskRepo.WithByID(taskID)) + if err != nil { + return + } + switch taskRecord.Status { + case constant.StatusFailed: + item.Status = "failed" + case constant.StatusExecuting, constant.StatusWaiting, constant.StatusRunning: + item.Status = "creating" + } +} diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index e1de98121..f5b2fd814 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -5220,7 +5220,7 @@ "formatZH": "更新 vLLM [name]", "formatEN": "update vLLM [name]" }, - "/xpack/vms": { + "/vms": { "bodyKeys": [ "name" ], @@ -5229,7 +5229,7 @@ "formatZH": "创建虚拟机 [name]", "formatEN": "create VM [name]" }, - "/xpack/vms/del": { + "/vms/del": { "bodyKeys": [ "id" ], @@ -5247,7 +5247,7 @@ "formatZH": "删除虚拟机 [name]", "formatEN": "delete VM [name]" }, - "/xpack/vms/disks/resize": { + "/vms/disks/resize": { "bodyKeys": [ "name", "target", @@ -5258,14 +5258,14 @@ "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB" }, - "/xpack/vms/environment/enable": { + "/vms/environment/enable": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "启用虚拟机运行环境", "formatEN": "enable VM runtime environment" }, - "/xpack/vms/iso": { + "/vms/iso": { "bodyKeys": [ "name", "type" @@ -5275,7 +5275,7 @@ "formatZH": "创建虚拟机镜像 [name] [type]", "formatEN": "create VM ISO [name] [type]" }, - "/xpack/vms/iso/del": { + "/vms/iso/del": { "bodyKeys": [ "id" ], @@ -5293,7 +5293,7 @@ "formatZH": "删除虚拟机镜像 [name]", "formatEN": "delete VM ISO [name]" }, - "/xpack/vms/iso/update": { + "/vms/iso/update": { "bodyKeys": [ "name", "type" @@ -5303,7 +5303,7 @@ "formatZH": "更新虚拟机镜像 [name] [type]", "formatEN": "update VM ISO [name] [type]" }, - "/xpack/vms/networks": { + "/vms/networks": { "bodyKeys": [ "name", "type" @@ -5313,7 +5313,7 @@ "formatZH": "创建虚拟机网络 [name] [type]", "formatEN": "create VM network [name] [type]" }, - "/xpack/vms/networks/del": { + "/vms/networks/del": { "bodyKeys": [ "id" ], @@ -5331,7 +5331,7 @@ "formatZH": "删除虚拟机网络 [name]", "formatEN": "delete VM network [name]" }, - "/xpack/vms/networks/update": { + "/vms/networks/update": { "bodyKeys": [ "id" ], @@ -5349,7 +5349,7 @@ "formatZH": "更新虚拟机网络 [name]", "formatEN": "update VM network [name]" }, - "/xpack/vms/operate": { + "/vms/operate": { "bodyKeys": [ "name", "operate" @@ -5359,14 +5359,14 @@ "formatZH": "操作虚拟机 [name] [operate]", "formatEN": "operate VM [name] [operate]" }, - "/xpack/vms/orphans/clean": { + "/vms/orphans/clean": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "清理虚拟机孤立数据", "formatEN": "clean VM orphaned data" }, - "/xpack/vms/rename": { + "/vms/rename": { "bodyKeys": [ "name", "newName" @@ -5376,7 +5376,7 @@ "formatZH": "重命名虚拟机 [name] 为 [newName]", "formatEN": "rename VM [name] to [newName]" }, - "/xpack/vms/snapshots": { + "/vms/snapshots": { "bodyKeys": [ "name", "snapshotName" @@ -5386,7 +5386,7 @@ "formatZH": "创建虚拟机 [name] 快照 [snapshotName]", "formatEN": "create VM [name] snapshot [snapshotName]" }, - "/xpack/vms/snapshots/del": { + "/vms/snapshots/del": { "bodyKeys": [ "name", "snapshotName" @@ -5396,7 +5396,7 @@ "formatZH": "删除虚拟机 [name] 快照 [snapshotName]", "formatEN": "delete VM [name] snapshot [snapshotName]" }, - "/xpack/vms/snapshots/recover": { + "/vms/snapshots/recover": { "bodyKeys": [ "name", "snapshotName" @@ -5406,7 +5406,7 @@ "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", "formatEN": "recover VM [name] snapshot [snapshotName]" }, - "/xpack/vms/storages": { + "/vms/storages": { "bodyKeys": [ "name", "type" @@ -5416,7 +5416,7 @@ "formatZH": "创建虚拟机存储 [name] [type]", "formatEN": "create VM storage [name] [type]" }, - "/xpack/vms/storages/del": { + "/vms/storages/del": { "bodyKeys": [ "id" ], @@ -5434,7 +5434,7 @@ "formatZH": "删除虚拟机存储 [name]", "formatEN": "delete VM storage [name]" }, - "/xpack/vms/storages/update": { + "/vms/storages/update": { "bodyKeys": [ "id" ], @@ -5452,14 +5452,14 @@ "formatZH": "更新虚拟机存储 [name]", "formatEN": "update VM storage [name]" }, - "/xpack/vms/sync": { + "/vms/sync": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "同步虚拟机", "formatEN": "sync virtual machines" }, - "/xpack/vms/templates": { + "/vms/templates": { "bodyKeys": [ "name", "templateName" @@ -5469,7 +5469,7 @@ "formatZH": "创建虚拟机 [name] 模板 [templateName]", "formatEN": "create VM [name] template [templateName]" }, - "/xpack/vms/templates/del": { + "/vms/templates/del": { "bodyKeys": [ "id" ], @@ -5487,7 +5487,7 @@ "formatZH": "删除虚拟机模板 [name]", "formatEN": "delete VM template [name]" }, - "/xpack/vms/update": { + "/vms/update": { "bodyKeys": [ "name" ], diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index d62649267..1417c0c45 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "The SSH public and private keys do not match" # virtual machine ErrVMNotFound: "Virtual machine does not exist" ErrVMAlreadyExists: "Virtual machine already exists" -ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more." +ErrVMCountLimit: "The current edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade your edition and try again." ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}" ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]" ErrVMHelperNotFound: "1panel-kvm not found" diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index f15ddc6a6..e48a9ac84 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden" # máquina virtual ErrVMNotFound: "La máquina virtual no existe" ErrVMAlreadyExists: "La máquina virtual ya existe" -ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo." +ErrVMCountLimit: "La edición actual permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o cambia a una edición superior y vuelve a intentarlo." ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}" ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]" ErrVMHelperNotFound: "No se encontró 1panel-kvm" diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 0effcda7b..31cae0221 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -737,7 +737,7 @@ ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندار # ماشین مجازی ErrVMNotFound: "ماشین مجازی وجود ندارد" ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد" -ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید." +ErrVMCountLimit: "در نسخه فعلی می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه بالاتر ارتقا دهید و دوباره تلاش کنید." ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}" ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است" ErrVMHelperNotFound: "1panel-kvm یافت نشد" diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 08a3e77ec..b6f0e4cdb 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません" # 仮想マシン ErrVMNotFound: "仮想マシンが存在しません" ErrVMAlreadyExists: "仮想マシンは既に存在します" -ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。" +ErrVMCountLimit: "現在のエディションでは最大 {{ .limit }} 台の仮想マシンを作成できます。 既存の仮想マシンを削除するか、上位エディションにアップグレードしてから再試行してください。" ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}" ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています" ErrVMHelperNotFound: "1panel-kvm が見つかりません" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index eb3980713..8f27066d0 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다" # 가상 머신 ErrVMNotFound: "가상 머신이 존재하지 않습니다" ErrVMAlreadyExists: "가상 머신이 이미 존재합니다" -ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요." +ErrVMCountLimit: "현재 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 상위 버전으로 업그레이드한 후 다시 시도하세요." ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}" ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다" ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다" diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index c9000d1d7..8c5e3ca89 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -727,7 +727,7 @@ ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນ # virtual machine ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ" ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ" -ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ." +ErrVMCountLimit: "ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດລຸ້ນ ແລ້ວລອງອີກຄັ້ງ." ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}" ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ" ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm" diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index d27179116..97c1349cd 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan" # mesin maya ErrVMNotFound: "Mesin maya tidak wujud" ErrVMAlreadyExists: "Mesin maya sudah wujud" -ErrVMCountLimit: "Edisi Professional membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf kepada Enterprise dan cuba lagi." +ErrVMCountLimit: "Edisi semasa membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf edisi anda dan cuba lagi." ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}" ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]" ErrVMHelperNotFound: "1panel-kvm tidak ditemui" diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index ebf583549..52375b5a3 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem" # máquina virtual ErrVMNotFound: "Máquina virtual não existe" ErrVMAlreadyExists: "Máquina virtual já existe" -ErrVMCountLimit: "A edição Professional permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para Enterprise e tente novamente." +ErrVMCountLimit: "A edição atual permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para uma edição superior e tente novamente." ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}" ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]" ErrVMHelperNotFound: "1panel-kvm não encontrado" diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 6685a61cf..ff0d08549 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не с # виртуальная машина ErrVMNotFound: "Виртуальная машина не существует" ErrVMAlreadyExists: "Виртуальная машина уже существует" -ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку." +ErrVMCountLimit: "В текущей редакции можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на старшую редакцию и повторите попытку." ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}" ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]" ErrVMHelperNotFound: "1panel-kvm не найден" diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index e5ac37c8c..393466f5c 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor" # sanal makine ErrVMNotFound: "Sanal makine mevcut değil" ErrVMAlreadyExists: "Sanal makine zaten mevcut" -ErrVMCountLimit: "Professional sürümü en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya Enterprise sürümüne yükseltip tekrar deneyin." +ErrVMCountLimit: "Mevcut sürüm en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya daha üst bir sürüme yükseltip tekrar deneyin." ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}" ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor" ErrVMHelperNotFound: "1panel-kvm bulunamadı" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index f405270c3..1ecb74426 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符" # 虛擬機 ErrVMNotFound: "虛擬機不存在" ErrVMAlreadyExists: "虛擬機已存在" -ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。" +ErrVMCountLimit: "目前版本最多可建立 {{ .limit }} 台虛擬機。 請刪除現有虛擬機或升級版本後重試。" ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}" ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用" ErrVMHelperNotFound: "1panel-kvm 不存在" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 66d8e2807..57d06d94a 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -736,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配" # 虚拟机 ErrVMNotFound: "虚拟机不存在" ErrVMAlreadyExists: "虚拟机已存在" -ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。" +ErrVMCountLimit: "当前版本最多可创建 {{ .limit }} 台虚拟机。 请删除已有虚拟机或升级版本后重试。" ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}" ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用" ErrVMHelperNotFound: "1panel-kvm 不存在" diff --git a/agent/init/kvm/kvm.go b/agent/init/kvm/kvm.go new file mode 100644 index 000000000..31b75b73d --- /dev/null +++ b/agent/init/kvm/kvm.go @@ -0,0 +1,18 @@ +package kvm + +import ( + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/global" + vmHelper "github.com/1Panel-dev/1Panel/agent/utils/vm/helper" +) + +func Init() { + if helperPath, embedded, err := vmHelper.EnsureEmbeddedHelper(); err != nil { + global.LOG.Warnf("sync embedded kvm helper failed, err: %v", err) + } else if embedded { + global.LOG.Infof("embedded kvm helper synced: %s", helperPath) + } + if err := service.NewIVMService().SyncResourcesOnStartup(); err != nil { + global.LOG.Warnf("sync VM resources on startup failed, err: %v", err) + } +} diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 3fed7a4ad..c8e34adf6 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -122,6 +122,7 @@ func agentDBMigrations() []*gormigrate.Migration { migrations.AddContainerImageCheckUpdates, migrations.AddModelQuickJump, migrations.MigrateModelDownloader, + migrations.MigrateVMDatabase, } } diff --git a/agent/init/migration/migrations/vm.go b/agent/init/migration/migrations/vm.go new file mode 100644 index 000000000..8ef5c41ab --- /dev/null +++ b/agent/init/migration/migrations/vm.go @@ -0,0 +1,107 @@ +package migrations + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/glebarez/sqlite" + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +var MigrateVMDatabase = &gormigrate.Migration{ + ID: "20261102-migrate-vm-to-community", + Migrate: func(db *gorm.DB) error { + return migrateVMDatabase(db, global.Dir.DbDir) + }, +} + +func migrateVMDatabase(db *gorm.DB, dbDir string) error { + var legacy *gorm.DB + legacyPath := filepath.Join(dbDir, "xpack.db") + if _, err := os.Stat(legacyPath); err == nil { + uri := (&url.URL{Scheme: "file", Path: legacyPath, RawQuery: "mode=ro"}).String() + legacy, err = gorm.Open(sqlite.Open(uri), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}) + if err != nil { + return fmt.Errorf("open legacy VM database: %w", err) + } + sqlDB, err := legacy.DB() + if err != nil { + return err + } + defer sqlDB.Close() + } else if !os.IsNotExist(err) { + return fmt.Errorf("inspect legacy VM database: %w", err) + } + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.VirtualMachine{}, &model.VMTemplate{}, &model.VMIso{}, &model.VMNetwork{}, &model.VMStorage{}); err != nil { + return err + } + if legacy != nil { + tables := []struct { + name string + records interface{} + }{ + {name: "virtual_machines", records: &[]model.VirtualMachine{}}, + {name: "vm_templates", records: &[]model.VMTemplate{}}, + {name: "vm_isos", records: &[]model.VMIso{}}, + {name: "vm_networks", records: &[]model.VMNetwork{}}, + {name: "vm_storages", records: &[]model.VMStorage{}}, + } + for _, table := range tables { + if err := migrateVMTable(legacy, tx, table.name, table.records); err != nil { + return fmt.Errorf("migrate %s: %w", table.name, err) + } + } + } + return nil + }) +} + +func migrateVMTable(source, target *gorm.DB, table string, records interface{}) error { + if !source.Migrator().HasTable(table) { + return nil + } + if err := target.Table(table).Where("1 = 1").Delete(records).Error; err != nil { + return err + } + query := source.Table(table).Find(records) + if query.Error != nil { + return query.Error + } + if query.RowsAffected != 0 { + if err := target.Table(table).CreateInBatches(records, 50).Error; err != nil { + return err + } + var migrated int64 + if err := target.Table(table).Count(&migrated).Error; err != nil { + return err + } + if migrated != query.RowsAffected { + return fmt.Errorf("expected %d records, migrated %d", query.RowsAffected, migrated) + } + } + return migrateVMSequence(source, target, table) +} + +func migrateVMSequence(source, target *gorm.DB, table string) error { + var sequenceTable int64 + if err := source.Raw("SELECT COUNT(*) FROM sqlite_master WHERE name = 'sqlite_sequence'").Scan(&sequenceTable).Error; err != nil { + return err + } + if sequenceTable == 0 { + return nil + } + var sequence int64 + if err := source.Raw("SELECT COALESCE(MAX(seq), 0) FROM sqlite_sequence WHERE name = ?", table).Scan(&sequence).Error; err != nil { + return err + } + if err := target.Exec("INSERT INTO sqlite_sequence (name, seq) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = ?)", table, sequence, table).Error; err != nil { + return err + } + return target.Exec("UPDATE sqlite_sequence SET seq = MAX(seq, ?) WHERE name = ?", sequence, table).Error +} diff --git a/agent/router/common.go b/agent/router/common.go index 8e3a83d9b..b1f66f609 100644 --- a/agent/router/common.go +++ b/agent/router/common.go @@ -2,6 +2,7 @@ package router func commonGroups() []CommonRouter { return []CommonRouter{ + &VMRouter{}, &DashboardRouter{}, &HostRouter{}, &ContainerRouter{}, diff --git a/agent/router/ro_vm.go b/agent/router/ro_vm.go new file mode 100644 index 000000000..e4fddc72e --- /dev/null +++ b/agent/router/ro_vm.go @@ -0,0 +1,59 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2" + "github.com/gin-gonic/gin" +) + +type VMRouter struct{} + +func (s *VMRouter) InitRouter(Router *gin.RouterGroup) { + vmRouter := Router.Group("vms") + baseApi := v2.ApiGroupApp.BaseApi + { + vmRouter.GET("/environment", baseApi.CheckVMEnvironment) + vmRouter.POST("/environment/enable", baseApi.EnableVMEnvironment) + vmRouter.POST("/search", baseApi.SearchVM) + vmRouter.POST("/sync", baseApi.SyncVMs) + vmRouter.POST("/info", baseApi.GetVM) + vmRouter.POST("/health/check", baseApi.HealthCheckVM) + vmRouter.POST("/stats", baseApi.GetVMStats) + vmRouter.POST("", baseApi.CreateVM) + vmRouter.POST("/templates/list", baseApi.ListVMTemplates) + vmRouter.POST("/templates", baseApi.CreateVMTemplate) + vmRouter.POST("/templates/del", baseApi.DeleteVMTemplate) + vmRouter.POST("/update", baseApi.UpdateVM) + vmRouter.POST("/disks/resize", baseApi.ResizeVMDisk) + vmRouter.POST("/rename", baseApi.RenameVM) + vmRouter.POST("/del", baseApi.DeleteVM) + vmRouter.POST("/snapshots/list", baseApi.ListVMSnapshots) + vmRouter.POST("/snapshots", baseApi.CreateVMSnapshot) + vmRouter.POST("/snapshots/recover", baseApi.RecoverVMSnapshot) + vmRouter.POST("/snapshots/del", baseApi.DeleteVMSnapshot) + vmRouter.POST("/orphans/scan", baseApi.ScanVMOrphans) + vmRouter.POST("/orphans/clean", baseApi.CleanVMOrphans) + vmRouter.POST("/operate", baseApi.OperateVM) + vmRouter.POST("/console", baseApi.GetVMConsole) + vmRouter.GET("/console/ws", baseApi.ProxyVMConsole) + + vmRouter.POST("/iso/search", baseApi.SearchVMIso) + vmRouter.POST("/iso/list", baseApi.ListVMIso) + vmRouter.POST("/iso", baseApi.CreateVMIso) + vmRouter.POST("/iso/import/start", baseApi.StartVMIsoImport) + vmRouter.POST("/iso/import/update", baseApi.UpdateVMIsoImport) + vmRouter.POST("/iso/update", baseApi.UpdateVMIso) + vmRouter.POST("/iso/del", baseApi.DeleteVMIso) + + vmRouter.POST("/networks/list", baseApi.ListVMNetworks) + vmRouter.GET("/networks/bridges", baseApi.ListVMNetworkBridges) + vmRouter.GET("/networks/interfaces", baseApi.ListVMNetworkInterfaces) + vmRouter.POST("/networks", baseApi.CreateVMNetwork) + vmRouter.POST("/networks/update", baseApi.UpdateVMNetwork) + vmRouter.POST("/networks/del", baseApi.DeleteVMNetwork) + + vmRouter.POST("/storages/list", baseApi.ListVMStorages) + vmRouter.POST("/storages", baseApi.CreateVMStorage) + vmRouter.POST("/storages/update", baseApi.UpdateVMStorage) + vmRouter.POST("/storages/del", baseApi.DeleteVMStorage) + } +} diff --git a/agent/server/server.go b/agent/server/server.go index e78701348..5af79494e 100644 --- a/agent/server/server.go +++ b/agent/server/server.go @@ -22,6 +22,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/init/dir" "github.com/1Panel-dev/1Panel/agent/init/firewall" "github.com/1Panel-dev/1Panel/agent/init/hook" + "github.com/1Panel-dev/1Panel/agent/init/kvm" "github.com/1Panel-dev/1Panel/agent/init/lang" "github.com/1Panel-dev/1Panel/agent/init/log" "github.com/1Panel-dev/1Panel/agent/init/migration" @@ -122,6 +123,7 @@ func Start() { global.LOG.Info("agent startup: firewall init scheduled") InitOthers() global.LOG.Info("agent startup: edition initialized") + kvm.Init() rootRouter := router.Routers() global.LOG.Info("agent startup: router initialized") diff --git a/agent/utils/vm/helper/embedded.go b/agent/utils/vm/helper/embedded.go new file mode 100644 index 000000000..9ec9a080b --- /dev/null +++ b/agent/utils/vm/helper/embedded.go @@ -0,0 +1,58 @@ +package helper + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + + "github.com/1Panel-dev/1Panel/agent/utils/vm/protocol" +) + +func EnsureEmbeddedHelper() (string, bool, error) { + bin, ok := embeddedHelperBytes() + if !ok { + return "", false, nil + } + target, err := embeddedHelperPath() + if err != nil { + return "", true, err + } + if sameFileContent(target, bin) { + if err := os.Chmod(target, 0755); err != nil { + return "", true, err + } + return target, true, nil + } + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return "", true, err + } + tmp := target + ".tmp" + if err := os.WriteFile(tmp, bin, 0755); err != nil { + return "", true, err + } + if err := os.Rename(tmp, target); err != nil { + _ = os.Remove(tmp) + return "", true, err + } + if err := os.Chmod(target, 0755); err != nil { + return "", true, err + } + return target, true, nil +} + +func embeddedHelperPath() (string, error) { + self, err := os.Executable() + if err != nil { + return "", fmt.Errorf("load executable path: %w", err) + } + return filepath.Join(filepath.Dir(self), protocol.HelperBinary), nil +} + +func sameFileContent(path string, expected []byte) bool { + current, err := os.ReadFile(path) + if err != nil { + return false + } + return sha256.Sum256(current) == sha256.Sum256(expected) +} diff --git a/agent/utils/vm/helper/embedded_amd64.go b/agent/utils/vm/helper/embedded_amd64.go new file mode 100644 index 000000000..7b1cfafa9 --- /dev/null +++ b/agent/utils/vm/helper/embedded_amd64.go @@ -0,0 +1,12 @@ +//go:build amd64 + +package helper + +import _ "embed" + +//go:embed bin/1panel-kvm-amd64 +var embeddedHelper []byte + +func embeddedHelperBytes() ([]byte, bool) { + return embeddedHelper, len(embeddedHelper) > 0 +} diff --git a/agent/utils/vm/helper/embedded_arm64.go b/agent/utils/vm/helper/embedded_arm64.go new file mode 100644 index 000000000..5836e921e --- /dev/null +++ b/agent/utils/vm/helper/embedded_arm64.go @@ -0,0 +1,12 @@ +//go:build arm64 + +package helper + +import _ "embed" + +//go:embed bin/1panel-kvm-arm64 +var embeddedHelper []byte + +func embeddedHelperBytes() ([]byte, bool) { + return embeddedHelper, len(embeddedHelper) > 0 +} diff --git a/agent/utils/vm/helper/embedded_unsupported.go b/agent/utils/vm/helper/embedded_unsupported.go new file mode 100644 index 000000000..87136284f --- /dev/null +++ b/agent/utils/vm/helper/embedded_unsupported.go @@ -0,0 +1,7 @@ +//go:build !amd64 && !arm64 + +package helper + +func embeddedHelperBytes() ([]byte, bool) { + return nil, false +} diff --git a/agent/utils/vm/helper/prepare.sh b/agent/utils/vm/helper/prepare.sh new file mode 100644 index 000000000..62f19357a --- /dev/null +++ b/agent/utils/vm/helper/prepare.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +helper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +mkdir -p "${helper_dir}/bin" + +for arch in amd64 arm64; do + target="${helper_dir}/bin/1panel-kvm-${arch}" + if [ -s "${target}" ]; then + continue + fi + download="${target}.download" + trap 'rm -f "${download}"' EXIT + curl --fail --location --retry 3 --connect-timeout 15 --max-time 300 \ + "https://resource.fit2cloud.com/1panel/resource/kvm/1panel-kvm-${arch}" \ + --output "${download}" + test -s "${download}" + chmod 755 "${download}" + mv "${download}" "${target}" +done diff --git a/agent/utils/vm/helper/provider.go b/agent/utils/vm/helper/provider.go new file mode 100644 index 000000000..7618a4c06 --- /dev/null +++ b/agent/utils/vm/helper/provider.go @@ -0,0 +1,549 @@ +package helper + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "sync" + "time" + + "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/vm/protocol" + "github.com/1Panel-dev/1Panel/agent/utils/vm/types" +) + +const ( + defaultTimeout = 2 * time.Minute + longRunningTimeout = 30 * time.Minute + slowActionThreshold = 5 * time.Second +) + +type Provider struct { + HelperPath string + URI string + Timeout time.Duration + mu sync.Mutex + versionOnce sync.Once + versionGate chan struct{} + checked bool +} + +func NewProvider() (*Provider, error) { + return &Provider{URI: protocol.DefaultURI, Timeout: defaultTimeout}, nil +} + +func (p *Provider) CheckEnvironment(ctx context.Context) ([]dto.VMCheckItem, error) { + var items []protocol.VMCheckItem + if err := p.call(ctx, protocol.ActionCheck, nil, &items); err != nil { + return nil, err + } + out := make([]dto.VMCheckItem, 0, len(items)) + for _, item := range items { + out = append(out, dto.VMCheckItem{ + Name: item.Name, + Passed: item.Passed, + Message: item.Message, + IsEnable: item.IsEnable, + IsExist: item.IsExist, + IsActive: item.IsActive, + }) + } + return out, nil +} + +func (p *Provider) HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) { + var result protocol.VMHealthCheckResult + if err := p.callWithTimeout( + ctx, + protocol.ActionHealthCheck, + protocol.VMHealthCheckRequest{Name: name}, + &result, + longRunningTimeout, + ); err != nil { + return dto.VMHealthCheckResult{}, err + } + out := dto.VMHealthCheckResult{ + Pass: result.Pass, + Warn: result.Warn, + Fail: result.Fail, + Items: make([]dto.VMHealthCheckItem, 0, len(result.Items)), + } + for _, item := range result.Items { + out.Items = append(out.Items, dto.VMHealthCheckItem{ + Level: item.Level, Check: item.Check, Reason: item.Reason, Params: item.Params, + Detail: item.Detail, FixCommand: item.FixCommand, + }) + } + return out, nil +} + +func (p *Provider) HelperStatus() bool { + if _, err := p.resolveHelperPath(); err != nil { + return false + } + return true +} + +func (p *Provider) List(ctx context.Context) ([]dto.VMInfo, error) { + var items []protocol.VMInfo + if err := p.call(ctx, protocol.ActionList, nil, &items); err != nil { + return nil, err + } + out := make([]dto.VMInfo, 0, len(items)) + for _, item := range items { + out = append(out, toDTO(item)) + } + return out, nil +} + +func (p *Provider) ListResources(ctx context.Context, resourceType string) ([]dto.VMResourceInfo, error) { + var items []protocol.VMResourceInfo + if err := p.call(ctx, protocol.ActionResources, protocol.VMResourceSearch{Type: resourceType}, &items); err != nil { + return nil, err + } + out := make([]dto.VMResourceInfo, 0, len(items)) + for _, item := range items { + out = append(out, dto.VMResourceInfo{ + Type: item.Type, Name: item.Name, SubType: item.SubType, BridgeName: item.BridgeName, SourceInterface: item.SourceInterface, + CIDR: item.CIDR, Gateway: item.Gateway, DHCPStart: item.DHCPStart, DHCPEnd: item.DHCPEnd, + Path: item.Path, Status: item.Status, AutoStart: item.AutoStart, Capacity: item.Capacity, Available: item.Available, + }) + } + return out, nil +} + +func (p *Provider) CreateResource(ctx context.Context, req types.VMResourceCreate) error { + return p.call(ctx, protocol.ActionCreateResource, protocol.VMResourceCreate{ + Type: req.Type, Name: req.Name, SubType: req.SubType, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface, + CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd, + Path: req.Path, AutoStart: req.AutoStart, + }, nil) +} + +func (p *Provider) UpdateResource(ctx context.Context, req types.VMResourceUpdate) error { + return p.call(ctx, protocol.ActionUpdateResource, protocol.VMResourceUpdate{ + Type: req.Type, Name: req.Name, AutoStart: req.AutoStart, Status: req.Status, + }, nil) +} + +func (p *Provider) DeleteResource(ctx context.Context, resourceType string, name string) error { + return p.call(ctx, protocol.ActionDeleteResource, protocol.VMResourceDelete{Type: resourceType, Name: name}, nil) +} + +func (p *Provider) ScanOrphans(ctx context.Context, protectedPaths []string) ([]dto.VMOrphanItem, error) { + var items []protocol.VMOrphanItem + if err := p.callWithTimeout(ctx, protocol.ActionScanOrphans, protocol.VMOrphanScan{ProtectedPaths: protectedPaths}, &items, longRunningTimeout); err != nil { + return nil, err + } + out := make([]dto.VMOrphanItem, 0, len(items)) + for _, item := range items { + out = append(out, dto.VMOrphanItem{Type: item.Type, Path: item.Path, Size: item.Size, ModifiedAt: item.ModifiedAt}) + } + return out, nil +} + +func (p *Provider) CleanOrphans(ctx context.Context, items []dto.VMOrphanItem, protectedPaths []string) ([]dto.VMOrphanItem, error) { + payload := protocol.VMOrphanClean{ProtectedPaths: protectedPaths, Items: make([]protocol.VMOrphanItem, 0, len(items))} + for _, item := range items { + payload.Items = append(payload.Items, protocol.VMOrphanItem{Type: item.Type, Path: item.Path, Size: item.Size, ModifiedAt: item.ModifiedAt}) + } + var removed []protocol.VMOrphanItem + if err := p.callWithTimeout(ctx, protocol.ActionCleanOrphans, payload, &removed, longRunningTimeout); err != nil { + return nil, err + } + out := make([]dto.VMOrphanItem, 0, len(removed)) + for _, item := range removed { + out = append(out, dto.VMOrphanItem{Type: item.Type, Path: item.Path, Size: item.Size, ModifiedAt: item.ModifiedAt}) + } + return out, nil +} + +func (p *Provider) Get(ctx context.Context, name string, checkDisks bool) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.call(ctx, protocol.ActionGet, protocol.VMInfoReq{Name: name, CheckDisks: checkDisks}, &item); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) Stats(ctx context.Context, name string) (dto.VMStats, error) { + var item protocol.VMStats + if err := p.call(ctx, protocol.ActionStats, protocol.VMName{Name: name}, &item); err != nil { + return dto.VMStats{}, err + } + return dto.VMStats{ + Name: item.Name, Status: item.Status, Timestamp: item.Timestamp, CPUTime: item.CPUTime, VCPU: item.VCPU, + Memory: dto.VMMemoryStats(item.Memory), Disk: dto.VMDiskStats(item.Disk), Network: dto.VMNetworkStats(item.Network), + }, nil +} + +func (p *Provider) Create(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.call(ctx, protocol.ActionCreate, toProtocolConfig(cfg), &item); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) CreateFromTemplate(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.callWithTimeout(ctx, protocol.ActionCreateFromTemplate, toProtocolConfig(cfg), &item, longRunningTimeout); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) CreateTemplate(ctx context.Context, req types.VMTemplateCreate) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.callWithTimeout(ctx, protocol.ActionCreateTemplate, protocol.VMTemplateCreate{ + Name: req.Name, SourceName: req.SourceName, DiskPath: req.DiskPath, StoragePool: req.StoragePool, + }, &item, longRunningTimeout); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) Update(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.call(ctx, protocol.ActionUpdate, toProtocolConfig(cfg), &item); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) ResizeDisk(ctx context.Context, req dto.VMDiskResize) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.callWithTimeout(ctx, protocol.ActionResizeDisk, protocol.VMDiskResize{Name: req.Name, Target: req.Target, SizeGiB: req.SizeGiB}, &item, longRunningTimeout); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) Rename(ctx context.Context, name string, newName string) (dto.VMInfo, error) { + var item protocol.VMInfo + if err := p.call(ctx, protocol.ActionRename, protocol.VMRename{Name: name, NewName: newName}, &item); err != nil { + return dto.VMInfo{}, err + } + return toDTO(item), nil +} + +func (p *Provider) Delete(ctx context.Context, name string, force bool, deleteDisk bool, protectedPaths []string) error { + return p.call(ctx, protocol.ActionDelete, protocol.VMDelete{Name: name, Force: force, DeleteDisk: deleteDisk, ProtectedPaths: protectedPaths}, nil) +} + +func (p *Provider) ListSnapshots(ctx context.Context, name string) ([]dto.VMSnapshotInfo, error) { + var items []protocol.VMSnapshotInfo + if err := p.call(ctx, protocol.ActionListSnapshots, protocol.VMName{Name: name}, &items); err != nil { + return nil, err + } + out := make([]dto.VMSnapshotInfo, 0, len(items)) + for _, item := range items { + out = append(out, dto.VMSnapshotInfo(item)) + } + return out, nil +} + +func (p *Provider) CreateSnapshot(ctx context.Context, name string, snapshotName string, description string) error { + return p.call(ctx, protocol.ActionCreateSnapshot, protocol.VMSnapshotReq{ + Name: name, SnapshotName: snapshotName, Description: description, + }, nil) +} + +func (p *Provider) RecoverSnapshot(ctx context.Context, name string, snapshotName string) error { + return p.call(ctx, protocol.ActionRecoverSnapshot, protocol.VMSnapshotReq{Name: name, SnapshotName: snapshotName}, nil) +} + +func (p *Provider) DeleteSnapshot(ctx context.Context, name string, snapshotName string) error { + return p.call(ctx, protocol.ActionDeleteSnapshot, protocol.VMSnapshotReq{Name: name, SnapshotName: snapshotName}, nil) +} + +func (p *Provider) Start(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionStart, name) +} + +func (p *Provider) Shutdown(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionShutdown, name) +} + +func (p *Provider) Reboot(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionReboot, name) +} + +func (p *Provider) Destroy(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionDestroy, name) +} + +func (p *Provider) Suspend(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionSuspend, name) +} + +func (p *Provider) Resume(ctx context.Context, name string) error { + return p.lifecycle(ctx, protocol.ActionResume, name) +} + +func (p *Provider) Console(ctx context.Context, name string) (*dto.VMGraphics, error) { + var item protocol.VMGraphics + if err := p.call(ctx, protocol.ActionConsole, protocol.VMName{Name: name}, &item); err != nil { + return nil, err + } + return (*dto.VMGraphics)(&item), nil +} + +func (p *Provider) lifecycle(ctx context.Context, action string, name string) error { + return p.call(ctx, action, protocol.VMName{Name: name}, nil) +} + +func (p *Provider) checkVersion(ctx context.Context) error { + var info protocol.VersionInfo + if err := p.call(ctx, protocol.ActionVersion, nil, &info); err != nil { + return err + } + if info.API != protocol.MinVersion { + return buserr.WithErr("ErrVMCommandFailed", fmt.Errorf("unsupported kvm helper api version: %s", info.API)) + } + return nil +} + +func (p *Provider) call(ctx context.Context, action string, payload interface{}, out interface{}) error { + return p.callWithTimeout(ctx, action, payload, out, p.Timeout) +} + +func (p *Provider) callWithTimeout(ctx context.Context, action string, payload interface{}, out interface{}, timeout time.Duration) (retErr error) { + startedAt := time.Now() + defer func() { + logAction(action, time.Since(startedAt), retErr) + }() + if ctx == nil { + ctx = context.Background() + } + if timeout == 0 { + timeout = defaultTimeout + } + runCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + if action != protocol.ActionVersion { + if err := p.ensureVersion(runCtx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return buserr.New("ErrCmdTimeout") + } + return err + } + } + + req := protocol.Request{Version: protocol.Version, Action: action, URI: p.URI, Payload: payload} + input, err := json.Marshal(req) + if err != nil { + return err + } + helperPath, err := p.resolveHelperPath() + if err != nil { + return err + } + cmd := exec.Command(helperPath) + cmd.Stdin = bytes.NewReader(input) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = runHelperCommand(runCtx, cmd) + if stderr.Len() > 0 && global.LOG != nil { + global.LOG.Warnf("kvm helper stderr: %s", stderr.String()) + } + if errors.Is(runCtx.Err(), context.DeadlineExceeded) { + return buserr.New("ErrCmdTimeout") + } + if err != nil { + return buserr.WithErr("ErrVMCommandFailed", fmt.Errorf("helper failed: %w, stderr: %s", err, stderr.String())) + } + var resp protocol.Response + if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil { + return buserr.WithErr("ErrVMCommandFailed", fmt.Errorf("invalid helper response: %w", err)) + } + if !resp.Success { + return mapHelperError(resp.Error) + } + if out == nil { + return nil + } + data, err := json.Marshal(resp.Data) + if err != nil { + return err + } + return json.Unmarshal(data, out) +} + +func logAction(action string, duration time.Duration, err error) { + if global.LOG == nil { + return + } + durationMs := duration.Milliseconds() + if err != nil { + global.LOG.Errorf("[kvm] action failed, action=%s, duration_ms=%d, err=%v", action, durationMs, err) + return + } + if duration >= slowActionThreshold { + global.LOG.Warnf("[kvm] slow action, action=%s, duration_ms=%d", action, durationMs) + } +} + +func runHelperCommand(ctx context.Context, cmd *exec.Cmd) error { + if err := ctx.Err(); err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + select { + case err := <-done: + return err + case <-ctx.Done(): + if cmd.Process != nil { + _ = cmd.Process.Signal(os.Interrupt) + } + timer := time.NewTimer(5 * time.Second) + defer timer.Stop() + select { + case err := <-done: + return err + case <-timer.C: + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + return <-done + } + } +} + +func (p *Provider) ensureVersion(ctx context.Context) error { + p.versionOnce.Do(func() { + p.versionGate = make(chan struct{}, 1) + }) + select { + case p.versionGate <- struct{}{}: + defer func() { <-p.versionGate }() + case <-ctx.Done(): + return ctx.Err() + } + if err := ctx.Err(); err != nil { + return err + } + if p.checked { + return nil + } + if err := p.checkVersion(ctx); err != nil { + return err + } + p.checked = true + return nil +} + +func (p *Provider) resolveHelperPath() (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.HelperPath != "" { + return p.HelperPath, nil + } + path, err := findHelperPath() + if err != nil { + return "", err + } + p.HelperPath = path + return path, nil +} + +func mapHelperError(item *protocol.Error) error { + if item == nil { + return buserr.New("ErrVMCommandFailed") + } + switch item.Code { + case "MAIN_DISK_DELETE_FAILED": + return item + case "VM_NOT_FOUND": + return buserr.New("ErrVMNotFound") + case "VM_ALREADY_EXISTS": + return buserr.New("ErrVMAlreadyExists") + case "VM_IMAGE_ALREADY_EXISTS": + return buserr.WithDetail("ErrVMImageAlreadyExists", item.Detail, errors.New(item.Detail)) + case "DISK_RESIZE_BLOCKED": + if item.Detail == "referenceCheck" { + return buserr.New("ErrVMDiskReferenceCheck") + } + if item.Detail == "referenced" { + return buserr.New("ErrVMDiskReferenced") + } + return buserr.New("ErrVMDiskResizeBlocked") + case "VM_RUNNING": + return buserr.New("ErrVMRunning") + case "LIBVIRT_CONNECT": + return buserr.WithDetail("ErrVMLibvirtConnect", item.Detail, errors.New(item.Detail)) + case "PERMISSION_DENIED": + return buserr.WithDetail("ErrVMPermission", item.Detail, errors.New(item.Detail)) + case "NETWORK_NOT_FOUND": + return buserr.WithDetail("ErrVMNetworkNotFound", item.Detail, errors.New(item.Detail)) + case "INVALID_PATH": + return buserr.WithDetail("ErrVMInvalidPath", item.Detail, errors.New(item.Detail)) + case "INVALID_INPUT": + return buserr.WithDetail("ErrVMInvalidInput", item.Detail, errors.New(item.Detail)) + default: + detail := item.Detail + if detail == "" { + detail = item.Message + } + return buserr.WithErr("ErrVMCommandFailed", errors.New(detail)) + } +} + +func findHelperPath() (string, error) { + if path := os.Getenv("ONEPANEL_KVM"); path != "" { + return path, nil + } + self, err := os.Executable() + if err == nil { + candidate := filepath.Join(filepath.Dir(self), protocol.HelperBinary) + if _, err := os.Stat(candidate); err == nil { + return candidate, nil + } + } + if path, err := exec.LookPath(protocol.HelperBinary); err == nil { + return path, nil + } + return "", buserr.WithErr("ErrVMCommandFailed", fmt.Errorf("%s not found", protocol.HelperBinary)) +} + +func toProtocolConfig(cfg types.VMConfig) protocol.VMConfig { + return protocol.VMConfig{ + Name: cfg.Name, CPU: cfg.CPU, MemoryMiB: cfg.MemoryMiB, DiskSizeGiB: cfg.DiskSizeGiB, + DiskBus: cfg.DiskBus, DiskPath: cfg.DiskPath, StoragePool: cfg.StoragePool, ISOPaths: cfg.ISOPaths, ImagePath: cfg.ImagePath, + Network: cfg.Network, BootOrder: cfg.BootOrder, AutoStart: cfg.AutoStart, + StartAfterCreate: cfg.StartAfterCreate, + } +} + +func toDTO(item protocol.VMInfo) dto.VMInfo { + out := dto.VMInfo{ + Name: item.Name, UUID: item.UUID, CPU: item.CPU, Memory: item.Memory, DiskSize: item.DiskSize, + DiskBus: item.DiskBus, DiskPath: item.DiskPath, StoragePool: item.StoragePool, ISOPaths: item.ISOPaths, ImagePath: item.ImagePath, + Network: item.Network, BootOrder: item.BootOrder, AutoStart: item.AutoStart, Status: item.Status, + } + for _, disk := range item.Disks { + out.Disks = append(out.Disks, dto.VMDisk(disk)) + } + for _, net := range item.Interfaces { + out.Interfaces = append(out.Interfaces, dto.VMInterface(net)) + } + if item.Graphics != nil { + graphics := dto.VMGraphics(*item.Graphics) + out.Graphics = &graphics + } + return out +} diff --git a/agent/utils/vm/protocol/protocol.go b/agent/utils/vm/protocol/protocol.go new file mode 100644 index 000000000..6a8214b97 --- /dev/null +++ b/agent/utils/vm/protocol/protocol.go @@ -0,0 +1,334 @@ +package protocol + +// VM helper protocol definition shared by the core caller and helper binary. + +const ( + Version = "1.2.0" + API = "1.2.0" + MinVersion = "1.2.0" + DefaultURI = "qemu:///system" + HelperBinary = "1panel-kvm" +) + +const ( + ActionVersion = "version" + ActionCheck = "check" + ActionHealthCheck = "health-check" + ActionList = "list" + ActionGet = "get" + ActionCreate = "create" + ActionUpdate = "update" + ActionResizeDisk = "resizeDisk" + ActionRename = "rename" + ActionDelete = "delete" + ActionStart = "start" + ActionShutdown = "shutdown" + ActionReboot = "reboot" + ActionDestroy = "destroy" + ActionSuspend = "suspend" + ActionResume = "resume" + ActionConsole = "console" + ActionStats = "stats" + ActionListSnapshots = "listSnapshots" + ActionCreateSnapshot = "createSnapshot" + ActionRecoverSnapshot = "recoverSnapshot" + ActionDeleteSnapshot = "deleteSnapshot" + ActionCreateTemplate = "createTemplate" + ActionCreateFromTemplate = "createFromTemplate" + ActionResources = "resources" + ActionCreateResource = "createResource" + ActionUpdateResource = "updateResource" + ActionDeleteResource = "deleteResource" + ActionScanOrphans = "scanOrphans" + ActionCleanOrphans = "cleanOrphans" +) + +type Request struct { + Version string `json:"version"` + Action string `json:"action"` + URI string `json:"uri,omitempty"` + Payload any `json:"payload,omitempty"` +} + +type Response struct { + Success bool `json:"success"` + Data any `json:"data,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Error struct { + Code string `json:"code"` + Message string `json:"message"` + Detail string `json:"detail,omitempty"` +} + +func (e *Error) Error() string { + if e == nil { + return "" + } + if e.Detail != "" { + return e.Message + ": " + e.Detail + } + return e.Message +} + +type VersionInfo struct { + Version string `json:"version"` + API string `json:"api"` +} + +type VMConfig struct { + Name string `json:"name"` + CPU int `json:"cpu"` + MemoryMiB int64 `json:"memoryMiB"` + DiskSizeGiB int64 `json:"diskSizeGiB"` + DiskBus string `json:"diskBus,omitempty"` + DiskPath string `json:"diskPath,omitempty"` + StoragePool string `json:"storagePool,omitempty"` + ISOPaths []string `json:"isoPaths"` + ImagePath string `json:"imagePath,omitempty"` + Network string `json:"network"` + BootOrder string `json:"bootOrder,omitempty"` + StartAfterCreate bool `json:"startAfterCreate"` + AutoStart bool `json:"autoStart"` +} + +type VMInfoReq struct { + Name string `json:"name"` + CheckDisks bool `json:"checkDisks,omitempty"` +} + +type VMName struct { + Name string `json:"name"` +} + +type VMDiskResize struct { + Name string `json:"name"` + Target string `json:"target"` + SizeGiB int64 `json:"sizeGiB"` +} + +type VMRename struct { + Name string `json:"name"` + NewName string `json:"newName"` +} + +type VMDelete struct { + Name string `json:"name"` + Force bool `json:"force"` + DeleteDisk bool `json:"deleteDisk"` + ProtectedPaths []string `json:"protectedPaths,omitempty"` +} + +type VMSnapshotReq struct { + Name string `json:"name"` + SnapshotName string `json:"snapshotName,omitempty"` + Description string `json:"description,omitempty"` +} + +type VMSnapshotInfo struct { + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"createdAt"` + State string `json:"state"` + Parent string `json:"parent"` + Path string `json:"path"` + Size int64 `json:"size"` + Current bool `json:"current"` + Children int `json:"children"` +} + +type VMTemplateCreate struct { + Name string `json:"name"` + SourceName string `json:"sourceName"` + DiskPath string `json:"diskPath,omitempty"` + StoragePool string `json:"storagePool,omitempty"` +} + +type VMOrphanScan struct { + ProtectedPaths []string `json:"protectedPaths,omitempty"` +} + +type VMOrphanItem struct { + Type string `json:"type"` + Path string `json:"path"` + Size int64 `json:"size"` + ModifiedAt int64 `json:"modifiedAt"` +} + +type VMOrphanClean struct { + Items []VMOrphanItem `json:"items"` + ProtectedPaths []string `json:"protectedPaths,omitempty"` +} + +type VMCheckItem struct { + Name string `json:"name"` + Passed bool `json:"passed"` + Message string `json:"message"` + IsEnable bool `json:"isEnable"` + IsExist bool `json:"isExist"` + IsActive bool `json:"isActive"` + Version string `json:"version"` +} + +const ( + HealthCheckLevelPass = "PASS" + HealthCheckLevelInfo = "INFO" + HealthCheckLevelWarn = "WARN" + HealthCheckLevelFail = "FAIL" +) + +type VMHealthCheckRequest struct { + Name string `json:"name"` +} + +type VMHealthCheckItem struct { + Level string `json:"level"` + Check string `json:"check"` + Reason string `json:"reason,omitempty"` + Params map[string]string `json:"params,omitempty"` + Detail string `json:"detail,omitempty"` + FixCommand string `json:"fixCommand,omitempty"` +} + +type VMHealthCheckResult struct { + Pass int `json:"pass"` + Warn int `json:"warn"` + Fail int `json:"fail"` + Items []VMHealthCheckItem `json:"items"` +} + +type VMResourceSearch struct { + Type string `json:"type"` +} + +type VMResourceInfo struct { + Type string `json:"type"` + Name string `json:"name"` + SubType string `json:"subType"` + BridgeName string `json:"bridgeName,omitempty"` + SourceInterface string `json:"sourceInterface,omitempty"` + CIDR string `json:"cidr,omitempty"` + Gateway string `json:"gateway,omitempty"` + DHCPStart string `json:"dhcpStart,omitempty"` + DHCPEnd string `json:"dhcpEnd,omitempty"` + Path string `json:"path,omitempty"` + Status string `json:"status"` + AutoStart bool `json:"autoStart"` + Capacity uint64 `json:"capacity,omitempty"` + Available uint64 `json:"available,omitempty"` +} + +type VMResourceCreate struct { + Type string `json:"type"` + Name string `json:"name"` + SubType string `json:"subType"` + BridgeName string `json:"bridgeName,omitempty"` + SourceInterface string `json:"sourceInterface,omitempty"` + CIDR string `json:"cidr,omitempty"` + Gateway string `json:"gateway,omitempty"` + DHCPStart string `json:"dhcpStart,omitempty"` + DHCPEnd string `json:"dhcpEnd,omitempty"` + Path string `json:"path,omitempty"` + AutoStart bool `json:"autoStart"` +} + +type VMResourceUpdate struct { + Type string `json:"type"` + Name string `json:"name"` + AutoStart *bool `json:"autoStart,omitempty"` + Status *string `json:"status,omitempty"` +} + +type VMResourceDelete struct { + Type string `json:"type"` + Name string `json:"name"` +} + +type VMInfo struct { + Name string `json:"name"` + UUID string `json:"uuid"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + DiskSize int64 `json:"diskSize"` + DiskBus string `json:"diskBus"` + DiskPath string `json:"diskPath"` + StoragePool string `json:"storagePool"` + ISOPaths []string `json:"isoPaths"` + ImagePath string `json:"imagePath"` + Network string `json:"network"` + BootOrder string `json:"bootOrder"` + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + Disks []VMDisk `json:"disks"` + Interfaces []VMInterface `json:"interfaces"` + Graphics *VMGraphics `json:"graphics"` +} + +type VMDisk struct { + Capacity int64 `json:"capacity"` + Format string `json:"format"` + ReadOnly bool `json:"readOnly"` + Shareable bool `json:"shareable"` + ResizeBlocked string `json:"resizeBlocked"` + Device string `json:"device"` + Type string `json:"type"` + Source string `json:"source"` + Target string `json:"target"` + Bus string `json:"bus"` +} + +type VMInterface struct { + Type string `json:"type"` + Source string `json:"source"` + Model string `json:"model"` + MAC string `json:"mac"` + IP string `json:"ip"` +} + +type VMGraphics struct { + Type string `json:"type"` + Port string `json:"port"` + Listen string `json:"listen"` +} + +type VMStats struct { + Name string `json:"name"` + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + CPUTime uint64 `json:"cpuTime"` + VCPU int `json:"vcpu"` + Memory VMMemoryStats `json:"memory"` + Disk VMDiskStats `json:"disk"` + Network VMNetworkStats `json:"network"` +} + +type VMMemoryStats struct { + AvailableKiB uint64 `json:"availableKiB"` + UnusedKiB uint64 `json:"unusedKiB"` + UsableKiB uint64 `json:"usableKiB"` + RSSKiB uint64 `json:"rssKiB"` + ActualKiB uint64 `json:"actualKiB"` +} + +type VMDiskStats struct { + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` + ReadReqs uint64 `json:"readReqs"` + WriteReqs uint64 `json:"writeReqs"` +} + +type VMNetworkStats struct { + RxBytes uint64 `json:"rxBytes"` + TxBytes uint64 `json:"txBytes"` + RxPackets uint64 `json:"rxPackets"` + TxPackets uint64 `json:"txPackets"` +} + +func OK(data any) Response { + return Response{Success: true, Data: data} +} + +func Fail(code string, message string, detail string) Response { + return Response{Success: false, Error: &Error{Code: code, Message: message, Detail: detail}} +} diff --git a/agent/utils/vm/types/vm.go b/agent/utils/vm/types/vm.go new file mode 100644 index 000000000..fb1c9f5f9 --- /dev/null +++ b/agent/utils/vm/types/vm.go @@ -0,0 +1,45 @@ +package types + +type VMConfig struct { + Name string + CPU int + MemoryMiB int64 + DiskSizeGiB int64 + DiskBus string + DiskPath string + StoragePool string + ISOPaths []string + ImagePath string + Network string + BootOrder string + StartAfterCreate bool + AutoStart bool +} + +type VMTemplateCreate struct { + Name string + SourceName string + DiskPath string + StoragePool string +} + +type VMResourceCreate struct { + Type string + Name string + SubType string + BridgeName string + SourceInterface string + CIDR string + Gateway string + DHCPStart string + DHCPEnd string + Path string + AutoStart bool +} + +type VMResourceUpdate struct { + Type string + Name string + AutoStart *bool + Status *string +} diff --git a/agent/utils/vm/vm_provider.go b/agent/utils/vm/vm_provider.go new file mode 100644 index 000000000..26a4c5abb --- /dev/null +++ b/agent/utils/vm/vm_provider.go @@ -0,0 +1,46 @@ +package vm + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/vm/helper" + "github.com/1Panel-dev/1Panel/agent/utils/vm/types" +) + +type VMProvider interface { + CheckEnvironment(ctx context.Context) ([]dto.VMCheckItem, error) + HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) + HelperStatus() bool + List(ctx context.Context) ([]dto.VMInfo, error) + ListResources(ctx context.Context, resourceType string) ([]dto.VMResourceInfo, error) + CreateResource(ctx context.Context, req types.VMResourceCreate) error + UpdateResource(ctx context.Context, req types.VMResourceUpdate) error + DeleteResource(ctx context.Context, resourceType string, name string) error + ScanOrphans(ctx context.Context, protectedPaths []string) ([]dto.VMOrphanItem, error) + CleanOrphans(ctx context.Context, items []dto.VMOrphanItem, protectedPaths []string) ([]dto.VMOrphanItem, error) + Get(ctx context.Context, name string, checkDisks bool) (dto.VMInfo, error) + Stats(ctx context.Context, name string) (dto.VMStats, error) + Create(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) + CreateFromTemplate(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) + CreateTemplate(ctx context.Context, req types.VMTemplateCreate) (dto.VMInfo, error) + Update(ctx context.Context, cfg types.VMConfig) (dto.VMInfo, error) + ResizeDisk(ctx context.Context, req dto.VMDiskResize) (dto.VMInfo, error) + Rename(ctx context.Context, name string, newName string) (dto.VMInfo, error) + Delete(ctx context.Context, name string, force bool, deleteDisk bool, protectedPaths []string) error + ListSnapshots(ctx context.Context, name string) ([]dto.VMSnapshotInfo, error) + CreateSnapshot(ctx context.Context, name string, snapshotName string, description string) error + RecoverSnapshot(ctx context.Context, name string, snapshotName string) error + DeleteSnapshot(ctx context.Context, name string, snapshotName string) error + Start(ctx context.Context, name string) error + Shutdown(ctx context.Context, name string) error + Reboot(ctx context.Context, name string) error + Destroy(ctx context.Context, name string) error + Suspend(ctx context.Context, name string) error + Resume(ctx context.Context, name string) error + Console(ctx context.Context, name string) (*dto.VMGraphics, error) +} + +func NewVMProvider() (VMProvider, error) { + return helper.NewProvider() +} diff --git a/agent/utils/xpack/community.go b/agent/utils/xpack/community.go index dfe31bb01..524c3ae74 100644 --- a/agent/utils/xpack/community.go +++ b/agent/utils/xpack/community.go @@ -2,8 +2,15 @@ package xpack -import "github.com/1Panel-dev/1Panel/agent/utils/xpack/helper" +import ( + "github.com/1Panel-dev/1Panel/agent/utils/xpack/helper" + "github.com/gin-gonic/gin" +) var AlertProvider = helper.NewIAlertProvider() var MultiNodeProvider = helper.NewIMultiNodeProvider() + +func VMCountLimit(_ *gin.Context) int { + return 1 +} diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 53cb5d69f..57b7dd695 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -27330,6 +27330,1876 @@ const docTemplate = `{ } } }, + "/vms": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Creates a VM from ISO images or a template. The target node allows 1 VM in Community, 5 in Professional, and unlimited VMs in Enterprise. All VM records count toward the quota, regardless of state; creation fails with ErrVMCountLimit when the quota is reached.\nReturns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create VM [name]", + "formatZH": "创建虚拟机 [name]", + "paramKeys": [] + } + } + }, + "/vms/console": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMGraphics" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine console information", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/console/ws": { + "get": { + "description": "Upgrades the connection to WebSocket and forwards VNC traffic.", + "parameters": [ + { + "description": "virtual machine name", + "in": "query", + "name": "name", + "required": true, + "type": "string" + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Connect virtual machine VNC console over WebSocket", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "virtual_machines", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM [name]", + "formatZH": "删除虚拟机 [name]", + "paramKeys": [] + } + } + }, + "/vms/disks/resize": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMDiskResize" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Expand virtual machine disk", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "target", + "sizeGiB" + ], + "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB", + "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", + "paramKeys": [] + } + } + }, + "/vms/environment": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMEnvironment" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Check virtual machine environment", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/environment/enable": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Enable virtual machine environment", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "enable VM runtime environment", + "formatZH": "启用虚拟机运行环境", + "paramKeys": [] + } + } + }, + "/vms/health/check": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMHealthCheckResult" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Check virtual machine health", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/info": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMInfoReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMInfo" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine details", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine ISO image record", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM ISO [name] [type]", + "formatZH": "创建虚拟机镜像 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/iso/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine ISO image", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_isos", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM ISO [name]", + "formatZH": "删除虚拟机镜像 [name]", + "paramKeys": [] + } + } + }, + "/vms/iso/import/start": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Creates an import record and returns its id.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoImportStart" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Start virtual machine ISO image import", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/import/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoImportUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine ISO image import status", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMIsoInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine ISO images", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/search": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchPageWithType" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.PageResult" + }, + { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMIsoInfo" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Page virtual machine ISO images", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine ISO image record", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "update VM ISO [name] [type]", + "formatZH": "更新虚拟机镜像 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/networks": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMNetworkCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM network [name] [type]", + "formatZH": "创建虚拟机网络 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/networks/bridges": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkBridgeItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List host network bridges for virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMResourceDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_networks", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM network [name]", + "formatZH": "删除虚拟机网络 [name]", + "paramKeys": [] + } + } + }, + "/vms/networks/interfaces": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkInterfaceItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List host network interfaces for virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine networks", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMNetworkUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_networks", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update VM network [name]", + "formatZH": "更新虚拟机网络 [name]", + "paramKeys": [] + } + } + }, + "/vms/operate": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMOperate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Operate virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "operate" + ], + "formatEN": "operate VM [name] [operate]", + "formatZH": "操作虚拟机 [name] [operate]", + "paramKeys": [] + } + } + }, + "/vms/orphans/clean": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMOrphanClean" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Clean orphaned virtual machine data", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "clean VM orphaned data", + "formatZH": "清理虚拟机孤立数据", + "paramKeys": [] + } + } + }, + "/vms/orphans/scan": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Scan orphaned virtual machine data", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/rename": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMRename" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Rename virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "newName" + ], + "formatEN": "rename VM [name] to [newName]", + "formatZH": "重命名虚拟机 [name] 为 [newName]", + "paramKeys": [] + } + } + }, + "/vms/search": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns the VM list and the target node quota: 1 for Community, 5 for Professional, and 0 for unlimited Enterprise.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.VMSearchResult" + }, + { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMInfo" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Page virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/snapshots": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "create VM [name] snapshot [snapshotName]", + "formatZH": "创建虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/snapshots/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "delete VM [name] snapshot [snapshotName]", + "formatZH": "删除虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/snapshots/list": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotSearch" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMSnapshotInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine snapshots", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/snapshots/recover": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Recover virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "recover VM [name] snapshot [snapshotName]", + "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/stats": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMStats" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine statistics", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/storages": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMStorageCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM storage [name] [type]", + "formatZH": "创建虚拟机存储 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/storages/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMResourceDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_storages", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM storage [name]", + "formatZH": "删除虚拟机存储 [name]", + "paramKeys": [] + } + } + }, + "/vms/storages/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMStorageItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine storage pools", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/storages/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMStorageUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_storages", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update VM storage [name]", + "formatZH": "更新虚拟机存储 [name]", + "paramKeys": [] + } + } + }, + "/vms/sync": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Sync virtual machines", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "sync virtual machines", + "formatZH": "同步虚拟机", + "paramKeys": [] + } + } + }, + "/vms/templates": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMTemplateCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine template", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "templateName" + ], + "formatEN": "create VM [name] template [templateName]", + "formatZH": "创建虚拟机 [name] 模板 [templateName]", + "paramKeys": [] + } + } + }, + "/vms/templates/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMTemplateDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine template", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_templates", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM template [name]", + "formatZH": "删除虚拟机模板 [name]", + "paramKeys": [] + } + } + }, + "/vms/templates/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMTemplateInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine templates", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/update": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update VM [name]", + "formatZH": "更新虚拟机 [name]", + "paramKeys": [] + } + } + }, "/websites": { "post": { "consumes": [ @@ -44822,6 +46692,1244 @@ const docTemplate = `{ }, "type": "object" }, + "dto.VMCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "minimum": 1, + "type": "integer" + }, + "description": { + "type": "string" + }, + "diskBus": { + "enum": [ + "sata", + "virtio" + ], + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "minimum": 1, + "type": "integer" + }, + "imagePath": { + "type": "string" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "minimum": 1, + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "osType": { + "enum": [ + "windows", + "linux", + "other" + ], + "type": "string" + }, + "startAfterCreate": { + "type": "boolean" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "templateID": { + "type": "integer" + } + }, + "required": [ + "cpu", + "diskSize", + "memory", + "name", + "network" + ], + "type": "object" + }, + "dto.VMDelete": { + "properties": { + "force": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMDisk": { + "properties": { + "bus": { + "type": "string" + }, + "capacity": { + "type": "integer" + }, + "device": { + "type": "string" + }, + "format": { + "type": "string" + }, + "readOnly": { + "type": "boolean" + }, + "resizeBlocked": { + "type": "string" + }, + "shareable": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMDiskResize": { + "properties": { + "name": { + "type": "string" + }, + "sizeGiB": { + "maximum": 8388607, + "minimum": 1, + "type": "integer" + }, + "target": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "sizeGiB", + "target" + ], + "type": "object" + }, + "dto.VMDiskStats": { + "properties": { + "readBytes": { + "type": "integer" + }, + "readReqs": { + "type": "integer" + }, + "writeBytes": { + "type": "integer" + }, + "writeReqs": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMEnvironment": { + "properties": { + "arch": { + "type": "string" + }, + "archSupported": { + "type": "boolean" + }, + "helperExist": { + "type": "boolean" + }, + "helperMessage": { + "type": "string" + }, + "kvmExist": { + "type": "boolean" + }, + "kvmMessage": { + "type": "string" + }, + "libvirtActive": { + "type": "boolean" + }, + "libvirtExist": { + "type": "boolean" + }, + "libvirtMessage": { + "type": "string" + }, + "ready": { + "type": "boolean" + }, + "storageExist": { + "type": "boolean" + }, + "storageMessage": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMGraphics": { + "properties": { + "listen": { + "type": "string" + }, + "port": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMHealthCheckItem": { + "properties": { + "check": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "fixCommand": { + "type": "string" + }, + "level": { + "type": "string" + }, + "params": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "reason": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMHealthCheckResult": { + "properties": { + "fail": { + "type": "integer" + }, + "items": { + "items": { + "$ref": "#/definitions/dto.VMHealthCheckItem" + }, + "type": "array" + }, + "pass": { + "type": "integer" + }, + "warn": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMInfo": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "type": "integer" + }, + "createTaskID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "diskBus": { + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "type": "integer" + }, + "disks": { + "items": { + "$ref": "#/definitions/dto.VMDisk" + }, + "type": "array" + }, + "graphics": { + "$ref": "#/definitions/dto.VMGraphics" + }, + "id": { + "type": "integer" + }, + "imagePath": { + "type": "string" + }, + "interfaces": { + "items": { + "$ref": "#/definitions/dto.VMInterface" + }, + "type": "array" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "status": { + "type": "string" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "uuid": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMInfoReq": { + "properties": { + "checkDisks": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMInterface": { + "properties": { + "ip": { + "type": "string" + }, + "mac": { + "type": "string" + }, + "model": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMIsoCreate": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMIsoDelete": { + "properties": { + "deleteFile": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMIsoImportStart": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "enum": [ + "uploading", + "downloading" + ], + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "enum": [ + "upload", + "download" + ], + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "name", + "path", + "status", + "type" + ], + "type": "object" + }, + "dto.VMIsoImportUpdate": { + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "enum": [ + "uploading", + "downloading", + "ready", + "failed" + ], + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "enum": [ + "upload", + "download" + ], + "type": "string" + } + }, + "required": [ + "id", + "status" + ], + "type": "object" + }, + "dto.VMIsoInfo": { + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isExist": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMIsoUpdate": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMMemoryStats": { + "properties": { + "actualKiB": { + "type": "integer" + }, + "availableKiB": { + "type": "integer" + }, + "rssKiB": { + "type": "integer" + }, + "unusedKiB": { + "type": "integer" + }, + "usableKiB": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMNetworkBridgeItem": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "recommended": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.VMNetworkCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bridgeName": { + "type": "string" + }, + "cidr": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dhcpEnd": { + "type": "string" + }, + "dhcpStart": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sourceInterface": { + "type": "string" + }, + "type": { + "enum": [ + "nat", + "bridge", + "macvtap" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "dto.VMNetworkIPUsage": { + "properties": { + "ip": { + "type": "string" + }, + "mac": { + "type": "string" + }, + "source": { + "type": "string" + }, + "vmName": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMNetworkInterfaceItem": { + "properties": { + "addresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMNetworkItem": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bridgeName": { + "type": "string" + }, + "cidr": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dhcpEnd": { + "type": "string" + }, + "dhcpStart": { + "type": "string" + }, + "error": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "registered": { + "type": "boolean" + }, + "sourceInterface": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "usedIPs": { + "items": { + "$ref": "#/definitions/dto.VMNetworkIPUsage" + }, + "type": "array" + }, + "vmCount": { + "type": "integer" + }, + "vms": { + "items": { + "$ref": "#/definitions/dto.VMResourceRef" + }, + "type": "array" + } + }, + "type": "object" + }, + "dto.VMNetworkStats": { + "properties": { + "rxBytes": { + "type": "integer" + }, + "rxPackets": { + "type": "integer" + }, + "txBytes": { + "type": "integer" + }, + "txPackets": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMNetworkUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMOperate": { + "properties": { + "name": { + "type": "string" + }, + "operate": { + "enum": [ + "start", + "shutdown", + "reboot", + "destroy", + "suspend", + "resume" + ], + "type": "string" + } + }, + "required": [ + "name", + "operate" + ], + "type": "object" + }, + "dto.VMOrphanClean": { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "dto.VMOrphanItem": { + "properties": { + "modifiedAt": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "enum": [ + "orphan_domain_disk", + "orphan_snapshot", + "partial_file" + ], + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "type": "object" + }, + "dto.VMRename": { + "properties": { + "name": { + "type": "string" + }, + "newName": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "newName" + ], + "type": "object" + }, + "dto.VMReq": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMResourceDelete": { + "properties": { + "force": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMResourceRef": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMSearchResult": { + "properties": { + "items": {}, + "limit": { + "description": "Maximum VM count for the target node; 0 means unlimited.", + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMSnapshotInfo": { + "properties": { + "children": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + }, + "current": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "state": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMSnapshotReq": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "snapshotName": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "snapshotName" + ], + "type": "object" + }, + "dto.VMSnapshotSearch": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMStats": { + "properties": { + "cpuTime": { + "type": "integer" + }, + "disk": { + "$ref": "#/definitions/dto.VMDiskStats" + }, + "memory": { + "$ref": "#/definitions/dto.VMMemoryStats" + }, + "name": { + "type": "string" + }, + "network": { + "$ref": "#/definitions/dto.VMNetworkStats" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "vcpu": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMStorageCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "dir" + ], + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMStorageItem": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "available": { + "type": "integer" + }, + "capacity": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "vmCount": { + "type": "integer" + }, + "vms": { + "items": { + "$ref": "#/definitions/dto.VMResourceRef" + }, + "type": "array" + } + }, + "type": "object" + }, + "dto.VMStorageUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMTaskResp": { + "properties": { + "taskID": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMTemplateCreate": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "templateName": { + "type": "string" + } + }, + "required": [ + "name", + "templateName" + ], + "type": "object" + }, + "dto.VMTemplateDelete": { + "properties": { + "deleteDisk": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMTemplateInfo": { + "properties": { + "bootOrder": { + "type": "string" + }, + "cpu": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "diskBus": { + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "isExist": { + "type": "boolean" + }, + "memory": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "sourceVM": { + "type": "string" + }, + "storagePool": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "minimum": 1, + "type": "integer" + }, + "description": { + "type": "string" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "minimum": 1, + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "cpu", + "memory", + "name", + "network" + ], + "type": "object" + }, "dto.VolumeCreate": { "properties": { "driver": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 6b4d8742b..bc9ef7902 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -26336,6 +26336,1876 @@ } } }, + "/vms": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Creates a VM from ISO images or a template. The target node allows 1 VM in Community, 5 in Professional, and unlimited VMs in Enterprise. All VM records count toward the quota, regardless of state; creation fails with ErrVMCountLimit when the quota is reached.\nReturns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create VM [name]", + "formatZH": "创建虚拟机 [name]", + "paramKeys": [] + } + } + }, + "/vms/console": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMGraphics" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine console information", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/console/ws": { + "get": { + "description": "Upgrades the connection to WebSocket and forwards VNC traffic.", + "parameters": [ + { + "description": "virtual machine name", + "in": "query", + "name": "name", + "required": true, + "type": "string" + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Connect virtual machine VNC console over WebSocket", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "virtual_machines", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM [name]", + "formatZH": "删除虚拟机 [name]", + "paramKeys": [] + } + } + }, + "/vms/disks/resize": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMDiskResize" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Expand virtual machine disk", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "target", + "sizeGiB" + ], + "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB", + "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", + "paramKeys": [] + } + } + }, + "/vms/environment": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMEnvironment" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Check virtual machine environment", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/environment/enable": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Enable virtual machine environment", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "enable VM runtime environment", + "formatZH": "启用虚拟机运行环境", + "paramKeys": [] + } + } + }, + "/vms/health/check": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMHealthCheckResult" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Check virtual machine health", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/info": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMInfoReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMInfo" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine details", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine ISO image record", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM ISO [name] [type]", + "formatZH": "创建虚拟机镜像 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/iso/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine ISO image", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_isos", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM ISO [name]", + "formatZH": "删除虚拟机镜像 [name]", + "paramKeys": [] + } + } + }, + "/vms/iso/import/start": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Creates an import record and returns its id.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoImportStart" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Start virtual machine ISO image import", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/import/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoImportUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine ISO image import status", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMIsoInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine ISO images", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/search": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchPageWithType" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.PageResult" + }, + { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMIsoInfo" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Page virtual machine ISO images", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/iso/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMIsoUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine ISO image record", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "update VM ISO [name] [type]", + "formatZH": "更新虚拟机镜像 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/networks": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMNetworkCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM network [name] [type]", + "formatZH": "创建虚拟机网络 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/networks/bridges": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkBridgeItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List host network bridges for virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMResourceDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_networks", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM network [name]", + "formatZH": "删除虚拟机网络 [name]", + "paramKeys": [] + } + } + }, + "/vms/networks/interfaces": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkInterfaceItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List host network interfaces for virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMNetworkItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine networks", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/networks/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMNetworkUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine network", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_networks", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update VM network [name]", + "formatZH": "更新虚拟机网络 [name]", + "paramKeys": [] + } + } + }, + "/vms/operate": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMOperate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Operate virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "operate" + ], + "formatEN": "operate VM [name] [operate]", + "formatZH": "操作虚拟机 [name] [operate]", + "paramKeys": [] + } + } + }, + "/vms/orphans/clean": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMOrphanClean" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Clean orphaned virtual machine data", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "clean VM orphaned data", + "formatZH": "清理虚拟机孤立数据", + "paramKeys": [] + } + } + }, + "/vms/orphans/scan": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Scan orphaned virtual machine data", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/rename": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMRename" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Rename virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "newName" + ], + "formatEN": "rename VM [name] to [newName]", + "formatZH": "重命名虚拟机 [name] 为 [newName]", + "paramKeys": [] + } + } + }, + "/vms/search": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns the VM list and the target node quota: 1 for Community, 5 for Professional, and 0 for unlimited Enterprise.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.VMSearchResult" + }, + { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMInfo" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Page virtual machines", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/snapshots": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "create VM [name] snapshot [snapshotName]", + "formatZH": "创建虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/snapshots/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "delete VM [name] snapshot [snapshotName]", + "formatZH": "删除虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/snapshots/list": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotSearch" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMSnapshotInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine snapshots", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/snapshots/recover": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMSnapshotReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Recover virtual machine snapshot", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "snapshotName" + ], + "formatEN": "recover VM [name] snapshot [snapshotName]", + "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", + "paramKeys": [] + } + } + }, + "/vms/stats": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMReq" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMStats" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load virtual machine statistics", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/storages": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMStorageCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create VM storage [name] [type]", + "formatZH": "创建虚拟机存储 [name] [type]", + "paramKeys": [] + } + } + }, + "/vms/storages/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMResourceDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_storages", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM storage [name]", + "formatZH": "删除虚拟机存储 [name]", + "paramKeys": [] + } + } + }, + "/vms/storages/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMStorageItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine storage pools", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/storages/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMStorageUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine storage pool", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_storages", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update VM storage [name]", + "formatZH": "更新虚拟机存储 [name]", + "paramKeys": [] + } + } + }, + "/vms/sync": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Sync virtual machines", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "sync virtual machines", + "formatZH": "同步虚拟机", + "paramKeys": [] + } + } + }, + "/vms/templates": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMTemplateCreate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create virtual machine template", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "templateName" + ], + "formatEN": "create VM [name] template [templateName]", + "formatZH": "创建虚拟机 [name] 模板 [templateName]", + "paramKeys": [] + } + } + }, + "/vms/templates/del": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMTemplateDelete" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete virtual machine template", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "vm_templates", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete VM template [name]", + "formatZH": "删除虚拟机模板 [name]", + "paramKeys": [] + } + } + }, + "/vms/templates/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.VMTemplateInfo" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List virtual machine templates", + "tags": [ + "VirtualMachine" + ] + } + }, + "/vms/update": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Returns taskID for tracking the asynchronous operation.", + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VMUpdate" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VMTaskResp" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update virtual machine", + "tags": [ + "VirtualMachine" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update VM [name]", + "formatZH": "更新虚拟机 [name]", + "paramKeys": [] + } + } + }, "/websites": { "post": { "consumes": [ @@ -43125,6 +44995,1244 @@ }, "type": "object" }, + "dto.VMCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "minimum": 1, + "type": "integer" + }, + "description": { + "type": "string" + }, + "diskBus": { + "enum": [ + "sata", + "virtio" + ], + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "minimum": 1, + "type": "integer" + }, + "imagePath": { + "type": "string" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "minimum": 1, + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "osType": { + "enum": [ + "windows", + "linux", + "other" + ], + "type": "string" + }, + "startAfterCreate": { + "type": "boolean" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "templateID": { + "type": "integer" + } + }, + "required": [ + "cpu", + "diskSize", + "memory", + "name", + "network" + ], + "type": "object" + }, + "dto.VMDelete": { + "properties": { + "force": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMDisk": { + "properties": { + "bus": { + "type": "string" + }, + "capacity": { + "type": "integer" + }, + "device": { + "type": "string" + }, + "format": { + "type": "string" + }, + "readOnly": { + "type": "boolean" + }, + "resizeBlocked": { + "type": "string" + }, + "shareable": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMDiskResize": { + "properties": { + "name": { + "type": "string" + }, + "sizeGiB": { + "maximum": 8388607, + "minimum": 1, + "type": "integer" + }, + "target": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "sizeGiB", + "target" + ], + "type": "object" + }, + "dto.VMDiskStats": { + "properties": { + "readBytes": { + "type": "integer" + }, + "readReqs": { + "type": "integer" + }, + "writeBytes": { + "type": "integer" + }, + "writeReqs": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMEnvironment": { + "properties": { + "arch": { + "type": "string" + }, + "archSupported": { + "type": "boolean" + }, + "helperExist": { + "type": "boolean" + }, + "helperMessage": { + "type": "string" + }, + "kvmExist": { + "type": "boolean" + }, + "kvmMessage": { + "type": "string" + }, + "libvirtActive": { + "type": "boolean" + }, + "libvirtExist": { + "type": "boolean" + }, + "libvirtMessage": { + "type": "string" + }, + "ready": { + "type": "boolean" + }, + "storageExist": { + "type": "boolean" + }, + "storageMessage": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMGraphics": { + "properties": { + "listen": { + "type": "string" + }, + "port": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMHealthCheckItem": { + "properties": { + "check": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "fixCommand": { + "type": "string" + }, + "level": { + "type": "string" + }, + "params": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "reason": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMHealthCheckResult": { + "properties": { + "fail": { + "type": "integer" + }, + "items": { + "items": { + "$ref": "#/definitions/dto.VMHealthCheckItem" + }, + "type": "array" + }, + "pass": { + "type": "integer" + }, + "warn": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMInfo": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "type": "integer" + }, + "createTaskID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "diskBus": { + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "type": "integer" + }, + "disks": { + "items": { + "$ref": "#/definitions/dto.VMDisk" + }, + "type": "array" + }, + "graphics": { + "$ref": "#/definitions/dto.VMGraphics" + }, + "id": { + "type": "integer" + }, + "imagePath": { + "type": "string" + }, + "interfaces": { + "items": { + "$ref": "#/definitions/dto.VMInterface" + }, + "type": "array" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "status": { + "type": "string" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "uuid": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMInfoReq": { + "properties": { + "checkDisks": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMInterface": { + "properties": { + "ip": { + "type": "string" + }, + "mac": { + "type": "string" + }, + "model": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMIsoCreate": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMIsoDelete": { + "properties": { + "deleteFile": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMIsoImportStart": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "enum": [ + "uploading", + "downloading" + ], + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "enum": [ + "upload", + "download" + ], + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "name", + "path", + "status", + "type" + ], + "type": "object" + }, + "dto.VMIsoImportUpdate": { + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "enum": [ + "uploading", + "downloading", + "ready", + "failed" + ], + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "enum": [ + "upload", + "download" + ], + "type": "string" + } + }, + "required": [ + "id", + "status" + ], + "type": "object" + }, + "dto.VMIsoInfo": { + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isExist": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "tempFileID": { + "type": "string" + }, + "tempFileType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMIsoUpdate": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMMemoryStats": { + "properties": { + "actualKiB": { + "type": "integer" + }, + "availableKiB": { + "type": "integer" + }, + "rssKiB": { + "type": "integer" + }, + "unusedKiB": { + "type": "integer" + }, + "usableKiB": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMNetworkBridgeItem": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "recommended": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.VMNetworkCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bridgeName": { + "type": "string" + }, + "cidr": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dhcpEnd": { + "type": "string" + }, + "dhcpStart": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sourceInterface": { + "type": "string" + }, + "type": { + "enum": [ + "nat", + "bridge", + "macvtap" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "dto.VMNetworkIPUsage": { + "properties": { + "ip": { + "type": "string" + }, + "mac": { + "type": "string" + }, + "source": { + "type": "string" + }, + "vmName": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMNetworkInterfaceItem": { + "properties": { + "addresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMNetworkItem": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bridgeName": { + "type": "string" + }, + "cidr": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dhcpEnd": { + "type": "string" + }, + "dhcpStart": { + "type": "string" + }, + "error": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "registered": { + "type": "boolean" + }, + "sourceInterface": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "usedIPs": { + "items": { + "$ref": "#/definitions/dto.VMNetworkIPUsage" + }, + "type": "array" + }, + "vmCount": { + "type": "integer" + }, + "vms": { + "items": { + "$ref": "#/definitions/dto.VMResourceRef" + }, + "type": "array" + } + }, + "type": "object" + }, + "dto.VMNetworkStats": { + "properties": { + "rxBytes": { + "type": "integer" + }, + "rxPackets": { + "type": "integer" + }, + "txBytes": { + "type": "integer" + }, + "txPackets": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMNetworkUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMOperate": { + "properties": { + "name": { + "type": "string" + }, + "operate": { + "enum": [ + "start", + "shutdown", + "reboot", + "destroy", + "suspend", + "resume" + ], + "type": "string" + } + }, + "required": [ + "name", + "operate" + ], + "type": "object" + }, + "dto.VMOrphanClean": { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.VMOrphanItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "dto.VMOrphanItem": { + "properties": { + "modifiedAt": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "enum": [ + "orphan_domain_disk", + "orphan_snapshot", + "partial_file" + ], + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "type": "object" + }, + "dto.VMRename": { + "properties": { + "name": { + "type": "string" + }, + "newName": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "newName" + ], + "type": "object" + }, + "dto.VMReq": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMResourceDelete": { + "properties": { + "force": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMResourceRef": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMSearchResult": { + "properties": { + "items": {}, + "limit": { + "description": "Maximum VM count for the target node; 0 means unlimited.", + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMSnapshotInfo": { + "properties": { + "children": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + }, + "current": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "state": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMSnapshotReq": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "snapshotName": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "name", + "snapshotName" + ], + "type": "object" + }, + "dto.VMSnapshotSearch": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "dto.VMStats": { + "properties": { + "cpuTime": { + "type": "integer" + }, + "disk": { + "$ref": "#/definitions/dto.VMDiskStats" + }, + "memory": { + "$ref": "#/definitions/dto.VMMemoryStats" + }, + "name": { + "type": "string" + }, + "network": { + "$ref": "#/definitions/dto.VMNetworkStats" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "vcpu": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.VMStorageCreate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "dir" + ], + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "type": "object" + }, + "dto.VMStorageItem": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "available": { + "type": "integer" + }, + "capacity": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "vmCount": { + "type": "integer" + }, + "vms": { + "items": { + "$ref": "#/definitions/dto.VMResourceRef" + }, + "type": "array" + } + }, + "type": "object" + }, + "dto.VMStorageUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMTaskResp": { + "properties": { + "taskID": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMTemplateCreate": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "storagePool": { + "type": "string" + }, + "taskID": { + "type": "string" + }, + "templateName": { + "type": "string" + } + }, + "required": [ + "name", + "templateName" + ], + "type": "object" + }, + "dto.VMTemplateDelete": { + "properties": { + "deleteDisk": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.VMTemplateInfo": { + "properties": { + "bootOrder": { + "type": "string" + }, + "cpu": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "diskBus": { + "type": "string" + }, + "diskPath": { + "type": "string" + }, + "diskSize": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "isExist": { + "type": "boolean" + }, + "memory": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "sourceVM": { + "type": "string" + }, + "storagePool": { + "type": "string" + } + }, + "type": "object" + }, + "dto.VMUpdate": { + "properties": { + "autoStart": { + "type": "boolean" + }, + "bootOrder": { + "type": "string" + }, + "cpu": { + "minimum": 1, + "type": "integer" + }, + "description": { + "type": "string" + }, + "isoPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "memory": { + "minimum": 1, + "type": "integer" + }, + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "cpu", + "memory", + "name", + "network" + ], + "type": "object" + }, "dto.VolumeCreate": { "properties": { "driver": { diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index e1de98121..f5b2fd814 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -5220,7 +5220,7 @@ "formatZH": "更新 vLLM [name]", "formatEN": "update vLLM [name]" }, - "/xpack/vms": { + "/vms": { "bodyKeys": [ "name" ], @@ -5229,7 +5229,7 @@ "formatZH": "创建虚拟机 [name]", "formatEN": "create VM [name]" }, - "/xpack/vms/del": { + "/vms/del": { "bodyKeys": [ "id" ], @@ -5247,7 +5247,7 @@ "formatZH": "删除虚拟机 [name]", "formatEN": "delete VM [name]" }, - "/xpack/vms/disks/resize": { + "/vms/disks/resize": { "bodyKeys": [ "name", "target", @@ -5258,14 +5258,14 @@ "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB" }, - "/xpack/vms/environment/enable": { + "/vms/environment/enable": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "启用虚拟机运行环境", "formatEN": "enable VM runtime environment" }, - "/xpack/vms/iso": { + "/vms/iso": { "bodyKeys": [ "name", "type" @@ -5275,7 +5275,7 @@ "formatZH": "创建虚拟机镜像 [name] [type]", "formatEN": "create VM ISO [name] [type]" }, - "/xpack/vms/iso/del": { + "/vms/iso/del": { "bodyKeys": [ "id" ], @@ -5293,7 +5293,7 @@ "formatZH": "删除虚拟机镜像 [name]", "formatEN": "delete VM ISO [name]" }, - "/xpack/vms/iso/update": { + "/vms/iso/update": { "bodyKeys": [ "name", "type" @@ -5303,7 +5303,7 @@ "formatZH": "更新虚拟机镜像 [name] [type]", "formatEN": "update VM ISO [name] [type]" }, - "/xpack/vms/networks": { + "/vms/networks": { "bodyKeys": [ "name", "type" @@ -5313,7 +5313,7 @@ "formatZH": "创建虚拟机网络 [name] [type]", "formatEN": "create VM network [name] [type]" }, - "/xpack/vms/networks/del": { + "/vms/networks/del": { "bodyKeys": [ "id" ], @@ -5331,7 +5331,7 @@ "formatZH": "删除虚拟机网络 [name]", "formatEN": "delete VM network [name]" }, - "/xpack/vms/networks/update": { + "/vms/networks/update": { "bodyKeys": [ "id" ], @@ -5349,7 +5349,7 @@ "formatZH": "更新虚拟机网络 [name]", "formatEN": "update VM network [name]" }, - "/xpack/vms/operate": { + "/vms/operate": { "bodyKeys": [ "name", "operate" @@ -5359,14 +5359,14 @@ "formatZH": "操作虚拟机 [name] [operate]", "formatEN": "operate VM [name] [operate]" }, - "/xpack/vms/orphans/clean": { + "/vms/orphans/clean": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "清理虚拟机孤立数据", "formatEN": "clean VM orphaned data" }, - "/xpack/vms/rename": { + "/vms/rename": { "bodyKeys": [ "name", "newName" @@ -5376,7 +5376,7 @@ "formatZH": "重命名虚拟机 [name] 为 [newName]", "formatEN": "rename VM [name] to [newName]" }, - "/xpack/vms/snapshots": { + "/vms/snapshots": { "bodyKeys": [ "name", "snapshotName" @@ -5386,7 +5386,7 @@ "formatZH": "创建虚拟机 [name] 快照 [snapshotName]", "formatEN": "create VM [name] snapshot [snapshotName]" }, - "/xpack/vms/snapshots/del": { + "/vms/snapshots/del": { "bodyKeys": [ "name", "snapshotName" @@ -5396,7 +5396,7 @@ "formatZH": "删除虚拟机 [name] 快照 [snapshotName]", "formatEN": "delete VM [name] snapshot [snapshotName]" }, - "/xpack/vms/snapshots/recover": { + "/vms/snapshots/recover": { "bodyKeys": [ "name", "snapshotName" @@ -5406,7 +5406,7 @@ "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", "formatEN": "recover VM [name] snapshot [snapshotName]" }, - "/xpack/vms/storages": { + "/vms/storages": { "bodyKeys": [ "name", "type" @@ -5416,7 +5416,7 @@ "formatZH": "创建虚拟机存储 [name] [type]", "formatEN": "create VM storage [name] [type]" }, - "/xpack/vms/storages/del": { + "/vms/storages/del": { "bodyKeys": [ "id" ], @@ -5434,7 +5434,7 @@ "formatZH": "删除虚拟机存储 [name]", "formatEN": "delete VM storage [name]" }, - "/xpack/vms/storages/update": { + "/vms/storages/update": { "bodyKeys": [ "id" ], @@ -5452,14 +5452,14 @@ "formatZH": "更新虚拟机存储 [name]", "formatEN": "update VM storage [name]" }, - "/xpack/vms/sync": { + "/vms/sync": { "bodyKeys": [], "paramKeys": [], "beforeFunctions": [], "formatZH": "同步虚拟机", "formatEN": "sync virtual machines" }, - "/xpack/vms/templates": { + "/vms/templates": { "bodyKeys": [ "name", "templateName" @@ -5469,7 +5469,7 @@ "formatZH": "创建虚拟机 [name] 模板 [templateName]", "formatEN": "create VM [name] template [templateName]" }, - "/xpack/vms/templates/del": { + "/vms/templates/del": { "bodyKeys": [ "id" ], @@ -5487,7 +5487,7 @@ "formatZH": "删除虚拟机模板 [name]", "formatEN": "delete VM template [name]" }, - "/xpack/vms/update": { + "/vms/update": { "bodyKeys": [ "name" ], diff --git a/core/constant/common.go b/core/constant/common.go index d40066cd1..b47ba272e 100644 --- a/core/constant/common.go +++ b/core/constant/common.go @@ -233,13 +233,13 @@ var WebUrlMap = map[string]struct{}{ "/enterprise/ops-report/alert": {}, "/enterprise/ops-report/history": {}, "/enterprise/ops-report/settings": {}, - "/xpack/vm": {}, - "/xpack/vm/overview": {}, - "/xpack/vm/list": {}, - "/xpack/vm/isos": {}, - "/xpack/vm/templates": {}, - "/xpack/vm/networks": {}, - "/xpack/vm/storage-pools": {}, + "/vm": {}, + "/vm/overview": {}, + "/vm/list": {}, + "/vm/isos": {}, + "/vm/templates": {}, + "/vm/networks": {}, + "/vm/storage-pools": {}, } var DynamicRoutes = []string{ diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index 7765a0076..4848f4889 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -120,6 +120,7 @@ func LoadMenus() string { {ID: "75", Disabled: false, Title: "menu.processManage", IsShow: true, Label: "Process", Path: "/hosts/process/process", Sort: 400}, {ID: "76", Disabled: false, Title: "menu.ssh", IsShow: true, Label: "SSH", Path: "/hosts/ssh/ssh", Sort: 500}, {ID: "77", Disabled: false, Title: "menu.disk", IsShow: true, Label: "Disk", Path: "/hosts/disk", Sort: 600}, + {ID: "123", Disabled: false, Title: "xpack.vm.title", IsShow: true, Label: "VirtualMachine", Path: "/vm", Sort: 900}, }}, {ID: "8", Disabled: false, Title: "menu.terminal", IsShow: true, Label: "Terminal-Menu", Path: "/hosts/terminal", Sort: 800}, {ID: "10", Disabled: false, Title: "menu.cronjob", IsShow: true, Label: "Cronjob-Menu", Path: "/cronjobs", Sort: 900}, @@ -131,7 +132,6 @@ func LoadMenus() string { {ID: "111", Disabled: false, Title: "xpack.node.nodeManagement", IsShow: true, Label: "NodeDashboard", Path: "/xpack/node/dashboard", Sort: 300}, {ID: "113", Disabled: false, Title: "xpack.monitor.name", IsShow: true, Label: "MonitorDashboard", Path: "/xpack/monitor/dashboard", Sort: 600}, {ID: "115", Disabled: false, Title: "xpack.sync.menu", IsShow: true, Label: "Sync", Path: "/xpack/sync", Sort: 700}, - {ID: "123", Disabled: false, Title: "xpack.vm.title", IsShow: true, Label: "VirtualMachine", Path: "/xpack/vm", Sort: 900}, {ID: "114", Disabled: false, Title: "xpack.tamper.tamper", IsShow: true, Label: "Tamper", Path: "/xpack/tamper", Sort: 1000}, {ID: "120", Disabled: false, Title: "xpack.cluster.cluster", IsShow: true, Label: "Cluster", Path: "/xpack/cluster", Sort: 1100}, {ID: "117", Disabled: false, Title: "xpack.setting.setting", IsShow: true, Label: "XSetting", Path: "/xpack/setting", Sort: 1200}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 379f7464f..35d8be847 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -73,5 +73,6 @@ func coreMigrations() []*gormigrate.Migration { migrations.UpdateAiMenuOrderV2, migrations.AddPanelMCP, migrations.AddAPIKeyDeletedAt, + migrations.MoveVirtualMachineMenuToCommunity, } } diff --git a/core/init/migration/migrations/vm.go b/core/init/migration/migrations/vm.go index 447f1f24e..c13ee0658 100644 --- a/core/init/migration/migrations/vm.go +++ b/core/init/migration/migrations/vm.go @@ -32,3 +32,45 @@ var MoveVirtualMachineMenuToXpack = &gormigrate.Migration{ }) }, } + +var MoveVirtualMachineMenuToCommunity = &gormigrate.Migration{ + ID: "20261102-move-vm-menu-to-community", + Migrate: func(tx *gorm.DB) error { + return helper.UpdateHideMenu(tx, func(menus []dto.ShowMenu) []dto.ShowMenu { + systemIndex := -1 + for i := range menus { + if menus[i].Label == "System-Menu" { + systemIndex = i + break + } + } + if systemIndex < 0 { + return menus + } + item := dto.ShowMenu{ID: "123", Title: "xpack.vm.title", IsShow: true, Label: "VirtualMachine", Path: "/vm", Sort: 900} + for i := range menus { + if menus[i].Label != "Xpack-Menu" { + continue + } + children := menus[i].Children[:0] + for _, child := range menus[i].Children { + if child.Label == "VirtualMachine" { + item = child + item.Path = "/vm" + continue + } + children = append(children, child) + } + menus[i].Children = children + } + for j := range menus[systemIndex].Children { + if menus[systemIndex].Children[j].Label == "VirtualMachine" { + menus[systemIndex].Children[j].Path = "/vm" + return menus + } + } + menus[systemIndex].Children = append(menus[systemIndex].Children, item) + return menus + }) + }, +} diff --git a/core/init/router/proxy.go b/core/init/router/proxy.go index 13dac611d..174fb1aa8 100644 --- a/core/init/router/proxy.go +++ b/core/init/router/proxy.go @@ -40,6 +40,7 @@ type agentProxyPolicy struct { func Proxy(policies ...agentProxyPolicy) gin.HandlerFunc { policies = append([]agentProxyPolicy(nil), policies...) return func(c *gin.Context) { + c.Request.Header.Del("X-1Panel-VM-Limit") terminalsession.ClearForwardedHeaders(c) mcpapi.ClearForwardedHeaders(c.Request.Header) for _, policy := range policies { @@ -122,6 +123,10 @@ func Proxy(policies ...agentProxyPolicy) gin.HandlerFunc { } } + if reqPath == "/api/v2/vms" || strings.HasPrefix(reqPath, "/api/v2/vms/") { + c.Request.Header.Set("X-1Panel-VM-Limit", strconv.Itoa(xpack.VMCountLimit(c))) + } + if reqPath == "/api/v2/hosts/terminal/local" && useLocalAgent { proxyLocalAgent(c) return diff --git a/core/middleware/demo_handle.go b/core/middleware/demo_handle.go index 7f0ae6a23..297274209 100644 --- a/core/middleware/demo_handle.go +++ b/core/middleware/demo_handle.go @@ -82,11 +82,11 @@ var demoAllowedRoutes = map[demoRoute]struct{}{ } var demoDeniedRoutes = map[demoRoute]struct{}{ - {http.MethodGet, "/api/v2/containers/exec"}: {}, - {http.MethodGet, "/api/v2/hosts/terminal/local"}: {}, - {http.MethodGet, "/api/v2/hosts/terminal/ssh"}: {}, - {http.MethodGet, "/api/v2/hosts/terminal/container"}: {}, - {http.MethodGet, "/api/v2/xpack/vms/console/ws"}: {}, + {http.MethodGet, "/api/v2/containers/exec"}: {}, + {http.MethodGet, "/api/v2/hosts/terminal/local"}: {}, + {http.MethodGet, "/api/v2/hosts/terminal/ssh"}: {}, + {http.MethodGet, "/api/v2/hosts/terminal/container"}: {}, + {http.MethodGet, "/api/v2/vms/console/ws"}: {}, } func DemoHandle() gin.HandlerFunc { diff --git a/core/utils/xpack/community.go b/core/utils/xpack/community.go index 802f3e4b8..54c99746d 100644 --- a/core/utils/xpack/community.go +++ b/core/utils/xpack/community.go @@ -2,8 +2,15 @@ package xpack -import "github.com/1Panel-dev/1Panel/core/utils/xpack/helper" +import ( + "github.com/1Panel-dev/1Panel/core/utils/xpack/helper" + "github.com/gin-gonic/gin" +) var AuthProvider = helper.NewIAuthProvider() var MultiNodeProvider = helper.NewIMultiNodeProvider() + +func VMCountLimit(_ *gin.Context) int { + return 1 +} diff --git a/frontend/src/api/interface/vm.ts b/frontend/src/api/interface/vm.ts new file mode 100644 index 000000000..f7de38467 --- /dev/null +++ b/frontend/src/api/interface/vm.ts @@ -0,0 +1,400 @@ +export namespace VM { + export interface Search { + info?: string; + } + + export interface TaskResp { + taskID: string; + } + + export interface Environment { + arch: string; + archSupported: boolean; + ready: boolean; + helperExist: boolean; + helperMessage: string; + kvmExist: boolean; + kvmMessage: string; + libvirtExist: boolean; + libvirtActive: boolean; + libvirtMessage: string; + storageExist: boolean; + storageMessage: string; + } + + export interface HealthCheckItem { + level: 'PASS' | 'INFO' | 'WARN' | 'FAIL'; + check: string; + reason?: string; + params?: Record; + detail?: string; + fixCommand?: string; + } + + export interface HealthCheckResult { + pass: number; + warn: number; + fail: number; + items: HealthCheckItem[]; + } + + export interface Disk { + capacity: number; + format: string; + readOnly: boolean; + shareable: boolean; + resizeBlocked: string; + device: string; + type: string; + source: string; + target: string; + bus: string; + } + + export interface NetInterface { + type: string; + source: string; + model: string; + mac: string; + ip: string; + } + + export interface Graphics { + type: string; + port: string; + listen: string; + url?: string; + } + + export interface Stats { + name: string; + status: string; + timestamp: number; + cpuTime: number; + vcpu: number; + memory: MemoryStats; + disk: DiskStats; + network: NetworkStats; + } + + export interface MemoryStats { + availableKiB: number; + unusedKiB: number; + usableKiB: number; + rssKiB: number; + actualKiB: number; + } + + export interface DiskStats { + readBytes: number; + writeBytes: number; + readReqs: number; + writeReqs: number; + } + + export interface NetworkStats { + rxBytes: number; + txBytes: number; + rxPackets: number; + txPackets: number; + } + + export interface Item { + id: number; + name: string; + uuid: string; + cpu: number; + /** Memory is measured in MiB. */ + memory: number; + diskSize: number; + diskBus: 'sata' | 'virtio' | string; + diskPath: string; + storagePool: string; + isoPaths: string[]; + imagePath: string; + network: string; + bootOrder: string; + autoStart: boolean; + status: string; + disks: Disk[]; + interfaces: NetInterface[]; + graphics?: Graphics; + taskID: string; + createTaskID: string; + description: string; + } + + export interface Create { + taskID?: string; + name: string; + cpu: number; + /** Memory is measured in MiB. */ + memory: number; + diskSize: number; + osType?: 'windows' | 'linux' | 'other'; + diskBus?: 'sata' | 'virtio'; + templateID?: number; + diskPath?: string; + storagePool?: string; + isoPaths: string[]; + imagePath?: string; + network: string; + bootOrder?: string; + startAfterCreate: boolean; + autoStart: boolean; + description?: string; + } + + export interface TemplateItem { + id: number; + name: string; + sourceVM: string; + cpu: number; + /** Memory is measured in MiB. */ + memory: number; + diskSize: number; + diskBus: 'sata' | 'virtio' | string; + diskPath: string; + storagePool: string; + network: string; + bootOrder: string; + isExist: boolean; + size: number; + status: 'uploading' | 'downloading' | 'ready' | 'failed' | string; + error: string; + tempFileID: string; + tempFileType: 'upload' | 'download' | string; + createdAt: string; + description: string; + } + + export interface TemplateCreate { + taskID?: string; + name: string; + templateName: string; + storagePool?: string; + description?: string; + } + + export interface TemplateDelete { + taskID?: string; + id: number; + deleteDisk: boolean; + } + + export interface Update { + taskID?: string; + name: string; + cpu: number; + /** Memory is measured in MiB. */ + memory: number; + isoPaths: string[]; + network: string; + bootOrder?: string; + autoStart: boolean; + description?: string; + } + + export interface DiskResize { + taskID?: string; + name: string; + target: string; + sizeGiB: number; + } + + export interface Rename { + taskID?: string; + name: string; + newName: string; + } + + export interface Delete { + taskID?: string; + id: number; + force: boolean; + } + + export interface SnapshotItem { + name: string; + description: string; + createdAt: number; + state: string; + parent: string; + path: string; + size: number; + current: boolean; + children: number; + } + + export interface SnapshotCreate { + taskID?: string; + name: string; + snapshotName: string; + description?: string; + } + + export interface SnapshotOperate { + taskID?: string; + name: string; + snapshotName: string; + } + + export interface IsoItem { + id: number; + type: string; + name: string; + path: string; + isExist: boolean; + size: number; + status: string; + error: string; + tempFileID: string; + tempFileType: string; + createdAt: string; + description: string; + } + + export interface IsoCreate { + type: string; + name: string; + path: string; + description?: string; + } + + export interface IsoUpdate { + id: number; + type: string; + name: string; + path: string; + description?: string; + } + + export interface IsoDelete { + id: number; + deleteFile?: boolean; + } + + export interface IsoImportStart extends IsoCreate { + status: 'uploading' | 'downloading'; + tempFileID?: string; + tempFileType?: 'upload' | 'download'; + } + + export interface IsoImportUpdate { + id: number; + status: 'uploading' | 'downloading' | 'ready' | 'failed'; + error?: string; + tempFileID?: string; + tempFileType?: 'upload' | 'download'; + } + + export interface ResourceDelete { + id: number; + force?: boolean; + } + + export interface NetworkItem { + id: number; + name: string; + type: 'nat' | 'bridge' | 'macvtap' | string; + bridgeName: string; + sourceInterface: string; + cidr: string; + gateway: string; + dhcpStart: string; + dhcpEnd: string; + autoStart: boolean; + description: string; + createdAt: string; + status: string; + error?: string; + vmCount: number; + vms: ResourceVM[]; + registered: boolean; + usedIPs: NetworkIPUsage[]; + } + + export interface ResourceVM { + id: number; + name: string; + } + + export interface NetworkIPUsage { + ip: string; + vmName: string; + mac: string; + source: string; + } + + export interface NetworkInterfaceItem { + name: string; + addresses: string[]; + } + + export interface NetworkBridgeItem { + name: string; + recommended: boolean; + description: string; + } + + export interface NetworkCreate { + name: string; + type: 'nat' | 'bridge' | 'macvtap'; + bridgeName?: string; + sourceInterface?: string; + cidr?: string; + gateway?: string; + dhcpStart?: string; + dhcpEnd?: string; + autoStart: boolean; + description?: string; + } + + export interface NetworkUpdate { + id: number; + autoStart?: boolean; + status?: string; + description?: string; + } + + export interface StorageItem { + id: number; + name: string; + type: 'dir' | string; + path: string; + autoStart: boolean; + description: string; + createdAt: string; + status: string; + error?: string; + capacity: number; + available: number; + usedPercent?: number; + vmCount: number; + vms: ResourceVM[]; + } + + export interface StorageCreate { + name: string; + type: 'dir'; + path: string; + autoStart: boolean; + description?: string; + } + + export interface StorageUpdate { + id: number; + name?: string; + path?: string; + autoStart?: boolean; + status?: string; + description?: string; + } + + export interface OrphanItem { + type: 'orphan_domain_disk' | 'orphan_snapshot' | 'partial_file'; + path: string; + size: number; + modifiedAt: number; + } +} diff --git a/frontend/src/api/modules/vm.ts b/frontend/src/api/modules/vm.ts new file mode 100644 index 000000000..1cbf0d486 --- /dev/null +++ b/frontend/src/api/modules/vm.ts @@ -0,0 +1,168 @@ +import http from '@/api'; +import { ResPage, SearchWithPage } from '@/api/interface'; +import { VM } from '@/api/interface/vm'; +import { TimeoutEnum } from '@/enums/http-enum'; + +export const checkVMEnvironment = () => { + return http.get('/vms/environment'); +}; + +export const enableVMEnvironment = () => { + return http.post('/vms/environment/enable', {}, TimeoutEnum.T_5M); +}; + +export const searchVMs = (params: SearchWithPage) => { + return http.post & { limit: number }>('/vms/search', params); +}; + +export const syncVMs = () => { + return http.post('/vms/sync'); +}; + +export const getVM = (name: string, checkDisks = false) => { + return http.post('/vms/info', { name, checkDisks }, checkDisks ? TimeoutEnum.T_3M : undefined); +}; + +export const healthCheckVM = (name: string, signal?: AbortSignal) => { + return http.postWithConfig( + '/vms/health/check', + { name }, + { timeout: TimeoutEnum.T_5M, signal }, + ); +}; + +export const getVMStats = (name: string) => { + return http.post('/vms/stats', { name }); +}; + +export const createVM = (params: VM.Create) => { + return http.post('/vms', params, TimeoutEnum.T_5M); +}; + +export const listVMTemplates = () => { + return http.post('/vms/templates/list'); +}; + +export const createVMTemplate = (params: VM.TemplateCreate) => { + return http.post('/vms/templates', params, TimeoutEnum.T_5M); +}; + +export const deleteVMTemplate = (params: VM.TemplateDelete) => { + return http.post('/vms/templates/del', params, TimeoutEnum.T_5M); +}; + +export const updateVM = (params: VM.Update) => { + return http.post('/vms/update', params, TimeoutEnum.T_5M); +}; + +export const resizeVMDisk = (params: VM.DiskResize) => { + return http.post('/vms/disks/resize', params, TimeoutEnum.T_5M); +}; + +export const renameVM = (params: VM.Rename) => { + return http.post('/vms/rename', params, TimeoutEnum.T_5M); +}; + +export const deleteVM = (params: VM.Delete) => { + return http.post('/vms/del', params, TimeoutEnum.T_5M); +}; + +export const listVMSnapshots = (name: string) => { + return http.post('/vms/snapshots/list', { name }); +}; + +export const createVMSnapshot = (params: VM.SnapshotCreate) => { + return http.post('/vms/snapshots', params, TimeoutEnum.T_5M); +}; + +export const recoverVMSnapshot = (params: VM.SnapshotOperate) => { + return http.post('/vms/snapshots/recover', params, TimeoutEnum.T_5M); +}; + +export const deleteVMSnapshot = (params: VM.SnapshotOperate) => { + return http.post('/vms/snapshots/del', params, TimeoutEnum.T_5M); +}; + +export const operateVM = (name: string, action: string) => { + return http.post(`/vms/operate`, { name: name, operate: action }); +}; + +export const getVMConsole = (name: string) => { + return http.post('/vms/console', { name }); +}; + +export const listVMIso = () => { + return http.post('/vms/iso/list'); +}; +export const searchVMIso = (params: SearchWithPage) => { + return http.post>('/vms/iso/search', params); +}; +export const createVMIso = (params: VM.IsoCreate, currentNode?: string) => { + return http.post('/vms/iso', params, TimeoutEnum.T_5M, currentNode ? { CurrentNode: currentNode } : undefined); +}; +export const startVMIsoImport = (params: VM.IsoImportStart, currentNode?: string) => { + return http.post<{ id: number }>( + '/vms/iso/import/start', + params, + undefined, + currentNode ? { CurrentNode: currentNode } : undefined, + ); +}; +export const updateVMIsoImport = (params: VM.IsoImportUpdate, currentNode?: string) => { + return http.post( + '/vms/iso/import/update', + params, + undefined, + currentNode ? { CurrentNode: currentNode } : undefined, + ); +}; +export const updateVMIso = (params: VM.IsoUpdate, currentNode?: string) => { + return http.post( + '/vms/iso/update', + params, + TimeoutEnum.T_5M, + currentNode ? { CurrentNode: currentNode } : undefined, + ); +}; +export const deleteVMIso = (params: VM.IsoDelete) => { + return http.post('/vms/iso/del', params, TimeoutEnum.T_5M); +}; + +export const listVMNetworks = () => { + return http.post('/vms/networks/list'); +}; +export const listVMNetworkBridges = () => { + return http.get('/vms/networks/bridges'); +}; +export const listVMNetworkInterfaces = () => { + return http.get('/vms/networks/interfaces'); +}; +export const createVMNetwork = (params: VM.NetworkCreate) => { + return http.post('/vms/networks', params, TimeoutEnum.T_5M); +}; +export const updateVMNetwork = (params: VM.NetworkUpdate) => { + return http.post('/vms/networks/update', params); +}; +export const deleteVMNetwork = (params: VM.ResourceDelete) => { + return http.post('/vms/networks/del', params, TimeoutEnum.T_5M); +}; + +export const listVMStorages = () => { + return http.post('/vms/storages/list'); +}; +export const createVMStorage = (params: VM.StorageCreate) => { + return http.post('/vms/storages', params, TimeoutEnum.T_5M); +}; +export const updateVMStorage = (params: VM.StorageUpdate) => { + return http.post('/vms/storages/update', params); +}; +export const deleteVMStorage = (params: VM.ResourceDelete) => { + return http.post('/vms/storages/del', params, TimeoutEnum.T_5M); +}; + +export const scanVMOrphans = () => { + return http.post('/vms/orphans/scan'); +}; +export const cleanVMOrphans = (items: VM.OrphanItem[]) => { + return http.post('/vms/orphans/clean', { items }, TimeoutEnum.T_5M); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 8c90e81ae..99018b565 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -3962,7 +3962,6 @@ const message = { monitor: 'Upgrade to Commercial Edition to view real-time website status, visitor trends, traffic sources, request logs, and more.', alert: 'Upgrade to Commercial Edition to receive SMS alerts, review alert logs, and stay on top of key events.', - vm: 'Upgrade to the commercial edition to manage virtual machines, images, templates, virtual networks, and storage resources in 1Panel.', node: 'Upgrade to Commercial Edition to manage multiple Linux servers in 1Panel.', nodeApp: 'Upgrade to Commercial Edition to upgrade multi-node app versions without switching nodes manually.', nodeDashboard: @@ -7537,8 +7536,7 @@ const message = { submitSuccess: 'Sync task submitted.', }, vm: { - countLimitHelper: - 'The Professional edition supports up to {0} virtual machines. Enterprise has no VM count limit.', + countLimitHelper: 'The current edition supports up to {0} virtual machines.', vm: 'VM', title: 'Virtual Machines', healthCheck: 'Health Check', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index c7a46c4e4..386319128 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4038,7 +4038,6 @@ const message = { setting: 'Actualizar a la edición comercial permite personalizar logo, mensaje de bienvenida y más.', monitor: 'La edición comercial permite ver estado web en tiempo real, tendencias, logs, etc.', alert: 'La edición comercial permite recibir alertas SMS y ver logs, control total de eventos clave.', - vm: 'Actualice a la edición comercial para gestionar máquinas virtuales, imágenes, plantillas, redes virtuales y recursos de almacenamiento en 1Panel.', node: 'Actualizar a la edición comercial permite administrar varios servidores Linux con 1Panel.', nodeApp: 'Actualizar a la edición comercial permite actualizar versiones de aplicaciones multinodo sin cambiar de nodo manualmente.', @@ -7657,8 +7656,7 @@ const message = { submitSuccess: 'La tarea de sincronización se ha enviado correctamente.', }, vm: { - countLimitHelper: - 'La edición Professional permite crear hasta {0} máquinas virtuales. Enterprise no limita la cantidad.', + countLimitHelper: 'La edición actual permite crear hasta {0} máquinas virtuales.', vm: 'Máquina virtual', title: 'Gestión de máquinas virtuales', healthCheck: 'Comprobación de estado', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 434f36545..286ce83b0 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -3922,7 +3922,6 @@ const message = { monitor: 'برای مشاهده وضعیت بلادرنگ وب‌سایت، روند بازدیدکنندگان، منابع ترافیک، لاگ‌های درخواست و موارد دیگر به نسخه تجاری ارتقاء دهید.', alert: 'برای دریافت هشدارهای پیامکی، مرور لاگ‌های هشدار و آگاهی از رویدادهای کلیدی به نسخه تجاری ارتقاء دهید.', - vm: 'برای مدیریت ماشین‌های مجازی، تصاویر، الگوها، شبکه‌های مجازی و منابع ذخیره‌سازی در 1Panel به نسخه تجاری ارتقا دهید.', node: 'برای مدیریت چندین سرور لینوکس در 1Panel به نسخه تجاری ارتقاء دهید.', nodeApp: 'برای ارتقاء نسخه برنامه‌های چند گره بدون نیاز به جابجایی دستی بین گره‌ها به نسخه تجاری ارتقاء دهید.', nodeDashboard: @@ -7490,8 +7489,7 @@ const message = { submitSuccess: 'وظیفه همگام‌سازی ارسال شد.', }, vm: { - countLimitHelper: - 'در نسخه Professional می‌توانید حداکثر {0} ماشین مجازی ایجاد کنید. نسخه Enterprise محدودیتی در تعداد ندارد.', + countLimitHelper: 'در نسخه فعلی می‌توانید حداکثر {0} ماشین مجازی ایجاد کنید.', vm: 'ماشین مجازی', title: 'مدیریت ماشین مجازی', healthCheck: 'بررسی سلامت', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 1a53f7cee..a8d8473af 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -3940,7 +3940,6 @@ const message = { '商用版にアップグレードすると、Web サイトのリアルタイム状態、訪問者トレンド、流入元、リクエストログなどを確認できます。', alert: '商用版にアップグレードすると、SMS アラートの受信やアラートログの確認が可能になります。', app: '商用版にアップグレードすると、モバイルアプリでサービス情報や異常監視を確認できます。', - vm: '商用版にアップグレードすると、1Panel で仮想マシン、イメージとテンプレート、仮想ネットワーク、ストレージリソースを一元管理できます。', node: '商用版にアップグレードすると、1Panel で複数の Linux サーバーを管理できます。', nodeApp: '商用版にアップグレードすると、ノードを手動で切り替えずにマルチノードアプリのバージョンを一括アップグレードできます。', @@ -7522,8 +7521,7 @@ const message = { submitSuccess: '同期タスクが送信されました。', }, vm: { - countLimitHelper: - 'Professional 版では最大 {0} 台の仮想マシンを作成できます。Enterprise 版には台数制限がありません。', + countLimitHelper: '現在のエディションでは最大 {0} 台の仮想マシンを作成できます。', vm: '仮想マシン', title: '仮想マシン管理', healthCheck: 'ヘルスチェック', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 89819f38b..9ba0de997 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -3866,7 +3866,6 @@ const message = { monitor: '상용 버전으로 업그레이드하면 웹사이트의 실시간 상태, 방문자 추세, 유입 경로, 요청 로그 등을 확인할 수 있습니다.', alert: '상용 버전으로 업그레이드하면 SMS 알림을 받고 알림 로그를 확인할 수 있습니다.', - vm: '상용 버전으로 업그레이드하면 1Panel에서 가상 머신, 이미지와 템플릿, 가상 네트워크 및 스토리지 리소스를 통합 관리할 수 있습니다.', node: '상용 버전으로 업그레이드하면 1Panel에서 여러 Linux 서버를 관리할 수 있습니다.', nodeApp: '상용 버전으로 업그레이드하면 노드를 수동으로 전환하지 않고도 다중 노드 애플리케이션 버전을 일괄 업그레이드할 수 있습니다.', @@ -7371,8 +7370,7 @@ const message = { submitSuccess: '동기화 작업이 제출되었습니다.', }, vm: { - countLimitHelper: - 'Professional 버전에서는 가상 머신을 최대 {0}대까지 생성할 수 있습니다. Enterprise 버전은 대수 제한이 없습니다.', + countLimitHelper: '현재 버전에서는 가상 머신을 최대 {0}대까지 생성할 수 있습니다.', vm: '가상 머신', title: '가상 머신 관리', healthCheck: '상태 점검', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 848478307..95ad0a29f 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -3847,7 +3847,6 @@ const message = { monitor: 'ອັບເກຣດເປັນເວີຊັນ Commercial ເພື່ອເບິ່ງສະຖານະເວັບໄຊແບບສົດໆ, ແນວໂນ້ມຜູ້ເຂົ້າຊົມ, ແຫຼ່ງທີ່ມາຂອງທຣາຟຟິກ, ບັນທຶກການຮ້ອງຂໍ ແລະ ອື່ນໆ.', alert: 'ອັບເກຣດເປັນເວີຊັນ Commercial ເພື່ອຮັບການແຈ້ງເຕືອນຜ່ານ SMS, ກວດສອບບັນທຶກການແຈ້ງເຕືອນ ແລະ ຕິດຕາມເຫດການສຳຄັນ.', - vm: 'ອັບເກຣດເປັນລຸ້ນທຸລະກິດເພື່ອຈັດການເຄື່ອງສະເໝືອນ, ອິມເມຈ, ແມ່ແບບ, ເຄືອຂ່າຍສະເໝືອນ ແລະ ຊັບພະຍາກອນຈັດເກັບໃນ 1Panel.', node: 'ອັບເກຣດເປັນເວີຊັນ Commercial ເພື່ອຈັດການເຊີບເວີ Linux ຫຼາຍເຄື່ອງໃນ 1Panel.', nodeApp: 'ອັບເກຣດເປັນເວີຊັນ Commercial ເພື່ອອັບເກຣດເວີຊັນແອັບໃນຫຼາຍໂນດໂດຍບໍ່ຕ້ອງສະຫຼັບໂນດດ້ວຍຕົນເອງ.', nodeDashboard: @@ -7283,8 +7282,7 @@ const message = { submitSuccess: 'ສົ່ງງານຊິງໂຄຣໄນສຳເລັດ.', }, vm: { - countLimitHelper: - 'ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {0} ເຄື່ອງ. ລຸ້ນ Enterprise ບໍ່ຈຳກັດຈຳນວນ.', + countLimitHelper: 'ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {0} ເຄື່ອງ.', vm: 'VM', title: 'ເຄື່ອງສະເໝືອນ', healthCheck: 'ກວດສອບສຸຂະພາບ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 1e5d1862d..949ee8420 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4021,7 +4021,6 @@ const message = { monitor: 'Naik taraf ke Edisi Komersial untuk melihat status laman web masa nyata, trend pelawat, sumber trafik, log permintaan dan lain-lain.', alert: 'Naik taraf ke Edisi Komersial untuk menerima amaran SMS dan melihat log amaran.', - vm: 'Naik taraf kepada edisi komersial untuk mengurus mesin maya, imej, templat, rangkaian maya dan sumber storan dalam 1Panel.', node: 'Naik taraf ke Edisi Komersial untuk mengurus berbilang pelayan Linux dengan 1Panel.', nodeApp: 'Naik taraf ke Edisi Komersial untuk menaik taraf versi aplikasi berbilang nod tanpa menukar nod secara manual.', @@ -7642,8 +7641,7 @@ const message = { submitSuccess: 'Tugas penyegerakan berjaya dihantar.', }, vm: { - countLimitHelper: - 'Edisi Professional membenarkan penciptaan sehingga {0} mesin maya. Edisi Enterprise tidak mengehadkan bilangannya.', + countLimitHelper: 'Edisi semasa membenarkan penciptaan sehingga {0} mesin maya.', vm: 'Mesin Maya', title: 'Pengurusan Mesin Maya', healthCheck: 'Pemeriksaan kesihatan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index d059d45a4..f69c3c0ae 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4034,7 +4034,6 @@ const message = { monitor: 'Faça upgrade para a edição comercial para acompanhar o status do site em tempo real, tendências de visitantes, origens de tráfego, logs de requisições e mais.', alert: 'Faça upgrade para a edição comercial para receber alertas por SMS e consultar os logs de alertas.', - vm: 'Atualize para a edição comercial para gerenciar máquinas virtuais, imagens, modelos, redes virtuais e recursos de armazenamento no 1Panel.', node: 'Atualize para a edição comercial para gerenciar vários servidores Linux com o 1Panel.', nodeApp: 'Atualize para a edição comercial para atualizar versões de aplicativos multi-nó sem alternar manualmente entre os nós.', @@ -7687,8 +7686,7 @@ const message = { submitSuccess: 'Tarefa de sincronização enviada com sucesso.', }, vm: { - countLimitHelper: - 'A edição Professional permite criar até {0} máquinas virtuais. A edição Enterprise não limita a quantidade.', + countLimitHelper: 'A edição atual permite criar até {0} máquinas virtuais.', vm: 'Máquina virtual', title: 'Gerenciamento de Máquinas Virtuais', healthCheck: 'Verificação de integridade', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index d4fec9b3b..73684003b 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4005,7 +4005,6 @@ const message = { monitor: 'Обновление до коммерческой версии позволяет просматривать статус веб-сайта в реальном времени, тенденции посещений, источники посетителей, логи запросов и другую информацию.', alert: 'Обновление до коммерческой версии позволяет получать информацию о тревогах через SMS и просматривать логи тревог, полностью контролировать различные ключевые события и обеспечивать беспроблемную работу системы', - vm: 'Перейдите на коммерческую версию, чтобы управлять виртуальными машинами, образами, шаблонами, виртуальными сетями и ресурсами хранения в 1Panel.', node: 'Обновление до коммерческой редакции позволяет управлять несколькими серверами Linux с помощью 1Panel.', nodeApp: 'Обновление до коммерческой редакции позволяет унифицированно обновлять версии приложений на нескольких узлах без необходимости ручного переключения узлов.', @@ -7659,8 +7658,7 @@ const message = { submitSuccess: 'Задача синхронизации успешно отправлена.', }, vm: { - countLimitHelper: - 'В редакции Professional можно создать не более {0} виртуальных машин. В Enterprise количество не ограничено.', + countLimitHelper: 'В текущей редакции можно создать не более {0} виртуальных машин.', vm: 'Виртуальная машина', title: 'Управление виртуальными машинами', healthCheck: 'Проверка состояния', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 33f5cecef..0b5ab680b 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4021,7 +4021,6 @@ const message = { monitor: 'Ticari sürüme yükselterek web sitesinin gerçek zamanlı durumunu, ziyaretçi eğilimlerini, kaynaklarını ve istek günlüklerini görüntüleyebilirsiniz.', alert: 'Ticari sürüme yükselterek SMS uyarıları alabilir, uyarı günlüklerini görüntüleyebilir ve kritik olayları takip edebilirsiniz.', - vm: '1Panel ile sanal makineleri, imajları, şablonları, sanal ağları ve depolama kaynaklarını yönetmek için ticari sürüme yükseltin.', node: 'Ticari sürüme yükselterek 1Panel ile birden fazla Linux sunucusunu yönetebilirsiniz.', nodeApp: 'Ticari sürüme yükselterek düğümleri manuel değiştirmeden çok düğümlü uygulama sürümlerini toplu olarak yükseltebilirsiniz.', @@ -7659,8 +7658,7 @@ const message = { submitSuccess: 'Senkronizasyon görevi başarıyla gönderildi.', }, vm: { - countLimitHelper: - 'Professional sürümü en fazla {0} sanal makine oluşturulmasına izin verir. Enterprise sürümünde sayı sınırı yoktur.', + countLimitHelper: 'Mevcut sürüm en fazla {0} sanal makine oluşturulmasına izin verir.', vm: 'Sanal Makine', title: 'Sanal Makine Yönetimi', healthCheck: 'Sağlık kontrolü', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 2da41819a..d6be47d1f 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3699,7 +3699,6 @@ const message = { setting: '升級商業版可以自訂面板 Logo、歡迎簡介等資訊。', monitor: '升級商業版可以檢視網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。', alert: '升級商業版可透過簡訊接收告警資訊,並檢視告警日誌,全面掌控各類關鍵事件,確保系統執行無憂。', - vm: '升級商業版,即可在 1Panel 中統一管理虛擬機、映像與範本、虛擬網路和儲存資源。', node: '升級商業版可以使用 1Panel 管理多台 Linux 伺服器。', nodeApp: '升級商業版可以統一升級多節點應用版本,無需手動切換節點。', nodeDashboard: '升級商業版可以集中管理多節點應用、網站、資料庫和計劃任務。', @@ -7004,7 +7003,7 @@ const message = { submitSuccess: '同步任務已提交。', }, vm: { - countLimitHelper: '專業版最多可建立 {0} 台虛擬機,企業版不限制數量。', + countLimitHelper: '目前版本最多可建立 {0} 台虛擬機。', vm: '虛擬機', title: '虛擬機管理', healthCheck: '健康檢查', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index b2d8703b5..1f94f4f35 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3729,7 +3729,6 @@ const message = { setting: '升级商业版可以自定义面板 Logo、欢迎简介等信息。', monitor: '升级商业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。', alert: '升级商业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。', - vm: '升级商业版,即可在 1Panel 中统一管理虚拟机、镜像与模板、虚拟网络和存储资源。', node: '升级商业版可以使用 1Panel 管理多台 linux 服务器。', nodeApp: '升级商业版可以统一升级多节点应用版本,无需手动切换节点。', nodeDashboard: '升级商业版可以集中管理多节点应用、网站、数据库和计划任务。', @@ -7061,7 +7060,7 @@ const message = { submitSuccess: '同步任务已提交。', }, vm: { - countLimitHelper: '专业版最多可创建 {0} 台虚拟机,企业版不限制数量。', + countLimitHelper: '当前版本最多可创建 {0} 台虚拟机。', vm: '虚拟机', title: '虚拟机管理', healthCheck: '健康检查', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 765019f7d..905e27494 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -1,4 +1,5 @@ import { Layout } from '@/routers/constant'; +import vmRouter from '@/routers/vm'; const hostRouter = { sort: 7, @@ -174,6 +175,7 @@ const hostRouter = { permission: 'host_ssh_view', }, }, + vmRouter, ], }; diff --git a/frontend/src/routers/vm.ts b/frontend/src/routers/vm.ts new file mode 100644 index 000000000..b97bba050 --- /dev/null +++ b/frontend/src/routers/vm.ts @@ -0,0 +1,89 @@ +const vmRouter = { + sort: 550, + path: '/vm', + name: 'VirtualMachine', + component: () => import('@/views/vm/index.vue'), + redirect: '/vm/overview', + meta: { + icon: 'p-disk-menu', + title: 'xpack.vm.title', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + children: [ + { + path: 'overview', + name: 'VirtualMachineOverview', + component: () => import('@/views/vm/overview/index.vue'), + hidden: true, + meta: { + title: 'xpack.vm.overview', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + { + path: 'list', + name: 'VirtualMachineList', + component: () => import('@/views/vm/list/index.vue'), + hidden: true, + meta: { + title: 'xpack.vm.title', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + { + path: 'isos', + name: 'VirtualMachineISOs', + component: () => import('@/views/vm/iso/iso.vue'), + hidden: true, + meta: { + title: 'xpack.vm.iso', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + { + path: 'templates', + name: 'VirtualMachineTemplates', + component: () => import('@/views/vm/template/index.vue'), + hidden: true, + meta: { + title: 'xpack.vm.template', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + { + path: 'networks', + name: 'VirtualMachineNetworks', + component: () => import('@/views/vm/network/index.vue'), + hidden: true, + meta: { + title: 'xpack.vm.network', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + { + path: 'storage-pools', + name: 'VirtualMachineStoragePools', + component: () => import('@/views/vm/storage/index.vue'), + hidden: true, + meta: { + title: 'xpack.vm.storagePool', + activeMenu: '/vm', + requiresAuth: true, + permission: 'xpack_vm_view', + }, + }, + ], +}; + +export default vmRouter; diff --git a/frontend/src/utils/vm.ts b/frontend/src/utils/vm.ts new file mode 100644 index 000000000..f14dd5224 --- /dev/null +++ b/frontend/src/utils/vm.ts @@ -0,0 +1,5 @@ +export const isVMResourceUnavailable = (status?: string) => + ['creating', 'failed', 'deleted'].includes(status?.trim().toLowerCase() || ''); + +export const isVMMemoryAllocated = (status: string) => + ['running', 'blocked', 'paused', 'shutdown', 'pmsuspended'].includes(status); diff --git a/frontend/src/views/vm/components/resource-delete.vue b/frontend/src/views/vm/components/resource-delete.vue new file mode 100644 index 000000000..3a99d8555 --- /dev/null +++ b/frontend/src/views/vm/components/resource-delete.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/frontend/src/views/vm/console/novnc.vue b/frontend/src/views/vm/console/novnc.vue new file mode 100644 index 000000000..9a6303280 --- /dev/null +++ b/frontend/src/views/vm/console/novnc.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/frontend/src/views/vm/index.vue b/frontend/src/views/vm/index.vue new file mode 100644 index 000000000..aa4ec1fd3 --- /dev/null +++ b/frontend/src/views/vm/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/src/views/vm/iso/create/index.vue b/frontend/src/views/vm/iso/create/index.vue new file mode 100644 index 000000000..73025c8c3 --- /dev/null +++ b/frontend/src/views/vm/iso/create/index.vue @@ -0,0 +1,873 @@ + + + + + diff --git a/frontend/src/views/vm/iso/iso.vue b/frontend/src/views/vm/iso/iso.vue new file mode 100644 index 000000000..d42b78aed --- /dev/null +++ b/frontend/src/views/vm/iso/iso.vue @@ -0,0 +1,420 @@ + + + + + diff --git a/frontend/src/views/vm/list/delete/index.vue b/frontend/src/views/vm/list/delete/index.vue new file mode 100644 index 000000000..1b4b883bf --- /dev/null +++ b/frontend/src/views/vm/list/delete/index.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/frontend/src/views/vm/list/detail/index.vue b/frontend/src/views/vm/list/detail/index.vue new file mode 100644 index 000000000..4e810816e --- /dev/null +++ b/frontend/src/views/vm/list/detail/index.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/views/vm/list/disks/index.vue b/frontend/src/views/vm/list/disks/index.vue new file mode 100644 index 000000000..ac604fe54 --- /dev/null +++ b/frontend/src/views/vm/list/disks/index.vue @@ -0,0 +1,156 @@ + + + diff --git a/frontend/src/views/vm/list/health-check/index.vue b/frontend/src/views/vm/list/health-check/index.vue new file mode 100644 index 000000000..7b4d6fa83 --- /dev/null +++ b/frontend/src/views/vm/list/health-check/index.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/frontend/src/views/vm/list/index.vue b/frontend/src/views/vm/list/index.vue new file mode 100644 index 000000000..7dd5b7815 --- /dev/null +++ b/frontend/src/views/vm/list/index.vue @@ -0,0 +1,1307 @@ + + + + + diff --git a/frontend/src/views/vm/list/monitor/index.vue b/frontend/src/views/vm/list/monitor/index.vue new file mode 100644 index 000000000..6294dc814 --- /dev/null +++ b/frontend/src/views/vm/list/monitor/index.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/frontend/src/views/vm/list/operate/index.vue b/frontend/src/views/vm/list/operate/index.vue new file mode 100644 index 000000000..1f2b39960 --- /dev/null +++ b/frontend/src/views/vm/list/operate/index.vue @@ -0,0 +1,698 @@ + + + + + diff --git a/frontend/src/views/vm/list/rename/index.vue b/frontend/src/views/vm/list/rename/index.vue new file mode 100644 index 000000000..cee3fc99a --- /dev/null +++ b/frontend/src/views/vm/list/rename/index.vue @@ -0,0 +1,74 @@ + + + diff --git a/frontend/src/views/vm/list/snapshot/index.vue b/frontend/src/views/vm/list/snapshot/index.vue new file mode 100644 index 000000000..fb20f14b4 --- /dev/null +++ b/frontend/src/views/vm/list/snapshot/index.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/frontend/src/views/vm/list/template/index.vue b/frontend/src/views/vm/list/template/index.vue new file mode 100644 index 000000000..bbe45443b --- /dev/null +++ b/frontend/src/views/vm/list/template/index.vue @@ -0,0 +1,118 @@ + + + diff --git a/frontend/src/views/vm/network/create/index.vue b/frontend/src/views/vm/network/create/index.vue new file mode 100644 index 000000000..7dcb710db --- /dev/null +++ b/frontend/src/views/vm/network/create/index.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/frontend/src/views/vm/network/index.vue b/frontend/src/views/vm/network/index.vue new file mode 100644 index 000000000..b90f6b20b --- /dev/null +++ b/frontend/src/views/vm/network/index.vue @@ -0,0 +1,528 @@ + + + + + diff --git a/frontend/src/views/vm/overview/index.vue b/frontend/src/views/vm/overview/index.vue new file mode 100644 index 000000000..72693dbbb --- /dev/null +++ b/frontend/src/views/vm/overview/index.vue @@ -0,0 +1,676 @@ + + + + + diff --git a/frontend/src/views/vm/overview/orphan-clean.vue b/frontend/src/views/vm/overview/orphan-clean.vue new file mode 100644 index 000000000..d0834efc6 --- /dev/null +++ b/frontend/src/views/vm/overview/orphan-clean.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/frontend/src/views/vm/status/index.vue b/frontend/src/views/vm/status/index.vue new file mode 100644 index 000000000..5c4af39b3 --- /dev/null +++ b/frontend/src/views/vm/status/index.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/frontend/src/views/vm/storage/create/index.vue b/frontend/src/views/vm/storage/create/index.vue new file mode 100644 index 000000000..c7c9f46b3 --- /dev/null +++ b/frontend/src/views/vm/storage/create/index.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/frontend/src/views/vm/storage/index.vue b/frontend/src/views/vm/storage/index.vue new file mode 100644 index 000000000..09a4eb908 --- /dev/null +++ b/frontend/src/views/vm/storage/index.vue @@ -0,0 +1,478 @@ + + + + + diff --git a/frontend/src/views/vm/template/index.vue b/frontend/src/views/vm/template/index.vue new file mode 100644 index 000000000..25459e05f --- /dev/null +++ b/frontend/src/views/vm/template/index.vue @@ -0,0 +1,312 @@ + + + + +