mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-02 16:05:23 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce429dbf67 | ||
|
|
e29679768b | ||
|
|
8e1b761f4c | ||
|
|
963c1463a8 | ||
|
|
2401bb16a2 | ||
|
|
c06c4e338b | ||
|
|
f8c61993c5 | ||
|
|
b31e683f1b | ||
|
|
50f0a37bee | ||
|
|
51cf40a648 | ||
|
|
7ec31cd5e9 | ||
|
|
50a45b72f3 | ||
|
|
13362f84a6 | ||
|
|
046dc94d05 | ||
|
|
2059119d89 | ||
|
|
7f4c503982 | ||
|
|
93ebc6a8b9 | ||
|
|
9a49f38ffd | ||
|
|
7156addf79 | ||
|
|
c6329a62db | ||
|
|
9644d5429f | ||
|
|
8b6328fd04 | ||
|
|
e9c2162921 |
@@ -79,3 +79,5 @@ opencode.json
|
||||
superpowers
|
||||
.worktrees/
|
||||
.codex/
|
||||
|
||||
/agent/utils/vm/helper/bin/1panel-kvm*
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) GetContainerImageCheckUpdateStatus(c *gin.Context) {
|
||||
status, err := service.GetContainerImageCheckUpdateStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, status)
|
||||
}
|
||||
func (b *BaseApi) ContainerImageCheckUpdate(c *gin.Context) {
|
||||
taskID, err := service.StartContainerImageCheckUpdate()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
status, err := service.GetContainerImageCheckUpdateStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
status.TaskID = taskID
|
||||
helper.SuccessWithData(c, status)
|
||||
}
|
||||
func (b *BaseApi) SetContainerImageCheckUpdateSchedule(c *gin.Context) {
|
||||
var req struct {
|
||||
Enabled *bool `json:"enabled" validate:"required"`
|
||||
}
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := service.SetContainerImageCheckUpdateSchedule(*req.Enabled); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
b.GetContainerImageCheckUpdateStatus(c)
|
||||
}
|
||||
@@ -11,16 +11,18 @@ var ApiGroupApp = new(ApiGroup)
|
||||
type BaseApi struct{}
|
||||
|
||||
var (
|
||||
vmService = service.NewIVMService()
|
||||
dashboardService = service.NewIDashboardService()
|
||||
|
||||
appService = service.NewIAppService()
|
||||
appInstallService = service.NewIAppInstalledService()
|
||||
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
|
||||
|
||||
aiToolService = service.NewIAIToolService()
|
||||
mcpServerService = service.NewIMcpServerService()
|
||||
tensorrtLLMService = service.NewITensorRTLLMService()
|
||||
agentService = service.NewIAgentService()
|
||||
modelDownloaderService = service.NewIModelDownloaderService()
|
||||
aiToolService = service.NewIAIToolService()
|
||||
mcpServerService = service.NewIMcpServerService()
|
||||
tensorrtLLMService = service.NewITensorRTLLMService()
|
||||
agentService = service.NewIAgentService()
|
||||
|
||||
containerService = service.NewIContainerService()
|
||||
composeTemplateService = service.NewIComposeTemplateService()
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -23,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
applyErrorMetadata(ctx, code, msgKey, err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Message: "",
|
||||
@@ -34,7 +33,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
}
|
||||
|
||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
applyErrorMetadata(ctx, code, msgKey, err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
ErrorCode: businessCode,
|
||||
@@ -45,7 +44,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri
|
||||
}
|
||||
|
||||
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
applyErrorMetadata(ctx, code, msgKey, err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Data: data,
|
||||
@@ -209,3 +208,26 @@ func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
|
||||
const errorMetadataKey = "error_response_metadata_policies"
|
||||
|
||||
// ErrorMetadataPolicy adds transport metadata before the existing error response is written.
|
||||
type ErrorMetadataPolicy func(*gin.Context, int, string, error)
|
||||
|
||||
func WithErrorMetadata(policies ...ErrorMetadataPolicy) gin.HandlerFunc {
|
||||
configured := append([]ErrorMetadataPolicy(nil), policies...)
|
||||
return func(c *gin.Context) {
|
||||
c.Set(errorMetadataKey, configured)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func applyErrorMetadata(c *gin.Context, code int, messageKey string, err error) {
|
||||
value, _ := c.Get(errorMetadataKey)
|
||||
policies, _ := value.([]ErrorMetadataPolicy)
|
||||
for _, policy := range policies {
|
||||
if policy != nil {
|
||||
policy(c, code, messageKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
_ "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" // Swagger response schema.
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -225,3 +229,30 @@ func (b *BaseApi) ImageLoad(c *gin.Context) {
|
||||
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Container Image
|
||||
// @Summary Search Docker Hub images
|
||||
// @Accept json
|
||||
// @Param request body dto.ImageRemoteSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dockerhub.SearchResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/image/remote/search [post]
|
||||
func (b *BaseApi) SearchRemoteImages(c *gin.Context) {
|
||||
var req dto.ImageRemoteSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
req.Query = strings.TrimSpace(req.Query)
|
||||
if req.Query == "" {
|
||||
helper.BadRequest(c, fmt.Errorf("search query is required"))
|
||||
return
|
||||
}
|
||||
result, err := imageService.SearchRemoteImages(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleModelDownloaderError(c *gin.Context, err error) {
|
||||
if service.IsModelDownloaderInputError(err) {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Search Hugging Face models
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.PageResult{items=[]dto.ModelItem}
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/hf/search [post]
|
||||
func (b *BaseApi) SearchHFModels(c *gin.Context) {
|
||||
var req dto.ModelSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := modelDownloaderService.SearchHFModels(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Total: total, Items: items})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Hugging Face model details
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelInfoReq true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/hf/info [post]
|
||||
func (b *BaseApi) GetHFModelInfo(c *gin.Context) {
|
||||
var req dto.ModelInfoReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := modelDownloaderService.GetHFModelInfo(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Download Hugging Face model
|
||||
// @Description Creates an asynchronous download task. Query /ai/model/downloader/tasks/search for progress.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownload true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelDownloadTaskItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/hf/download [post]
|
||||
// @x-panel-log {"bodyKeys":["repoID","revision"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 Hugging Face 模型 [repoID]","formatEN":"download Hugging Face model [repoID]"}
|
||||
func (b *BaseApi) StartHFModelDownload(c *gin.Context) {
|
||||
var req dto.ModelDownload
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
task, err := modelDownloaderService.StartDownload(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, task)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Search ModelScope models
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.PageResult{items=[]dto.ModelItem}
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/modelscope/search [post]
|
||||
func (b *BaseApi) SearchModelScopeModels(c *gin.Context) {
|
||||
var req dto.ModelSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := modelDownloaderService.SearchModelScopeModels(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Total: total, Items: items})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get ModelScope model details
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelInfoReq true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/modelscope/info [post]
|
||||
func (b *BaseApi) GetModelScopeModelInfo(c *gin.Context) {
|
||||
var req dto.ModelInfoReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := modelDownloaderService.GetModelScopeModelInfo(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Download ModelScope model
|
||||
// @Description Creates an asynchronous download task. Query /ai/model/downloader/tasks/search for progress.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownload true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelDownloadTaskItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/modelscope/download [post]
|
||||
// @x-panel-log {"bodyKeys":["repoID","revision"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 ModelScope 模型 [repoID]","formatEN":"download ModelScope model [repoID]"}
|
||||
func (b *BaseApi) StartModelScopeModelDownload(c *gin.Context) {
|
||||
var req dto.ModelDownload
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
task, err := modelDownloaderService.StartModelScopeDownload(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, task)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get model downloader settings
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelDownloaderSetting
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/settings [get]
|
||||
func (b *BaseApi) GetModelDownloaderSettings(c *gin.Context) {
|
||||
settings, err := modelDownloaderService.GetSettings()
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, settings)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update model downloader settings
|
||||
// @Description Updates the download directory, source endpoints and tokens. The featuredModelsSync field is ignored; use /ai/model/downloader/featured/sync/status to update automatic synchronization.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloaderSetting true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/settings [post]
|
||||
// @x-panel-log {"bodyKeys":["modelDir"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新模型下载设置 [modelDir]","formatEN":"update model downloader settings [modelDir]"}
|
||||
func (b *BaseApi) UpdateModelDownloaderSettings(c *gin.Context) {
|
||||
var req dto.ModelDownloaderSetting
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := modelDownloaderService.UpdateSettings(req); err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page model download tasks
|
||||
// @Description Set includeLocal to true to include locally downloaded models.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloadTaskSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.PageResult{items=[]dto.ModelDownloadTaskItem}
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/tasks/search [post]
|
||||
func (b *BaseApi) PageModelDownloadTasks(c *gin.Context) {
|
||||
var req dto.ModelDownloadTaskSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := modelDownloaderService.PageTasks(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Cancel model download task
|
||||
// @Description Only waiting or downloading tasks can be canceled. The withFile field is ignored.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloadTaskOperate true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/tasks/cancel [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"取消模型下载任务 [repoID]","formatEN":"cancel model download task [repoID]"}
|
||||
func (b *BaseApi) CancelModelDownloadTask(c *gin.Context) {
|
||||
var req dto.ModelDownloadTaskOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := modelDownloaderService.CancelTask(req); err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Retry model download task
|
||||
// @Description Only failed or canceled tasks can be retried. Returns the task while the download runs asynchronously.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloadTaskRetry true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ModelDownloadTaskItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/tasks/retry [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"重试模型下载任务 [repoID]","formatEN":"retry model download task [repoID]"}
|
||||
func (b *BaseApi) RetryModelDownloadTask(c *gin.Context) {
|
||||
var req dto.ModelDownloadTaskRetry
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
task, err := modelDownloaderService.RetryTask(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, task)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Remove model download task
|
||||
// @Description Waiting or downloading tasks cannot be removed. Set withFile to true to also delete downloaded files.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloadTaskOperate true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/tasks/remove [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"移除模型下载任务 [repoID]","formatEN":"remove model download task [repoID]"}
|
||||
func (b *BaseApi) RemoveModelDownloadTask(c *gin.Context) {
|
||||
var req dto.ModelDownloadTaskOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := modelDownloaderService.RemoveTask(req); err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page locally downloaded models
|
||||
// @Accept json
|
||||
// @Param request body dto.LocalModelSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.PageResult{items=[]dto.LocalModelItem}
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/local/search [post]
|
||||
func (b *BaseApi) PageLocalDownloadedModels(c *gin.Context) {
|
||||
var req dto.LocalModelSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := modelDownloaderService.ListLocalModels(req)
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete locally downloaded model
|
||||
// @Accept json
|
||||
// @Param request body dto.LocalModelDelete true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/local/delete [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除本地模型 [name]","formatEN":"delete local model [name]"}
|
||||
func (b *BaseApi) DeleteLocalDownloadedModel(c *gin.Context) {
|
||||
var req dto.LocalModelDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := modelDownloaderService.DeleteLocalModel(req); err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Synchronize featured models
|
||||
// @Description Synchronizes the featured model catalog immediately, independently of the automatic synchronization setting. Does nothing in offline mode.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/featured/sync [post]
|
||||
func (b *BaseApi) SyncFeaturedModels(c *gin.Context) {
|
||||
if err := modelDownloaderService.SyncFeaturedModels(); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page featured models
|
||||
// @Accept json
|
||||
// @Param request body dto.FeaturedModelSearch true "request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.FeaturedModelPage{items=[]dto.FeaturedModel}
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/featured/search [post]
|
||||
func (b *BaseApi) PageFeaturedModels(c *gin.Context) {
|
||||
var req dto.FeaturedModelSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := modelDownloaderService.PageFeaturedModels(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update featured model automatic synchronization
|
||||
// @Description Set value to Enable or Disable. This updates the automatic synchronization setting without triggering an immediate synchronization.
|
||||
// @Accept json
|
||||
// @Param request body dto.ModelDownloaderSyncSetting true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/model/downloader/featured/sync/status [post]
|
||||
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新推荐模型自动同步 [value]","formatEN":"update featured model automatic synchronization [value]"}
|
||||
func (b *BaseApi) UpdateFeaturedModelsSync(c *gin.Context) {
|
||||
var req dto.ModelDownloaderSyncSetting
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := modelDownloaderService.UpdateFeaturedModelsSync(req); err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -19,7 +19,11 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
list := tensorrtLLMService.Page(req)
|
||||
list, err := tensorrtLLMService.Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := tensorrtLLMService.Delete(req.ID)
|
||||
err := tensorrtLLMService.Delete(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+16
-13
@@ -318,6 +318,7 @@ type AgentAccountModelDiscoverReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountModelCreateReq struct {
|
||||
TaskID string `json:"taskID"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model AgentAccountModel `json:"model" validate:"required"`
|
||||
}
|
||||
@@ -328,8 +329,9 @@ type AgentAccountModelUpdateReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountModelDeleteReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
RecordID uint `json:"recordId" validate:"required"`
|
||||
DeleteFiles bool `json:"deleteFiles"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
RecordID uint `json:"recordId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountCreateReq struct {
|
||||
@@ -347,17 +349,18 @@ type AgentAccountCreateReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountUpdateReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
Models []AgentAccountModel `json:"models"`
|
||||
}
|
||||
|
||||
type AgentAccountVerifyReq struct {
|
||||
|
||||
@@ -21,13 +21,16 @@ type InspectReq struct {
|
||||
}
|
||||
|
||||
type ContainerInfo struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
Name string `json:"name"`
|
||||
ImageId string `json:"imageID"`
|
||||
ImageName string `json:"imageName"`
|
||||
CreateTime string `json:"createTime"`
|
||||
State string `json:"state"`
|
||||
RunTime string `json:"runTime"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
TargetImage string `json:"targetImage"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
ContainerID string `json:"containerID"`
|
||||
Name string `json:"name"`
|
||||
ImageId string `json:"imageID"`
|
||||
ImageName string `json:"imageName"`
|
||||
CreateTime string `json:"createTime"`
|
||||
State string `json:"state"`
|
||||
RunTime string `json:"runTime"`
|
||||
|
||||
Network []string `json:"network"`
|
||||
Ports []string `json:"ports"`
|
||||
|
||||
@@ -2,6 +2,12 @@ package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type ImageRemoteSearch struct {
|
||||
Query string `json:"query" validate:"required,max=256"`
|
||||
Page int `json:"page" validate:"required,min=1"`
|
||||
PageSize int `json:"pageSize" validate:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type PageImage struct {
|
||||
PageInfo
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package dto
|
||||
|
||||
type ModelSearch struct {
|
||||
Query string `json:"query"`
|
||||
Sort string `json:"sort" validate:"omitempty,oneof=trending downloads updated created likes most_params least_params"`
|
||||
Page int `json:"page" validate:"omitempty,min=1"`
|
||||
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=100"`
|
||||
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
|
||||
}
|
||||
|
||||
type ModelInfoReq struct {
|
||||
RepoID string `json:"repoID" validate:"required"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
|
||||
type ModelDownload struct {
|
||||
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
|
||||
RepoID string `json:"repoID" validate:"required"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
|
||||
type ModelDownloaderSyncSetting struct {
|
||||
Value string `json:"value" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type ModelDownloaderSetting struct {
|
||||
FeaturedModelsSync string `json:"featuredModelsSync"`
|
||||
ModelDir string `json:"modelDir" validate:"required"`
|
||||
HFEndpoint string `json:"hfEndpoint"`
|
||||
HFToken string `json:"hfToken"`
|
||||
ModelScopeEndpoint string `json:"modelScopeEndpoint"`
|
||||
ModelScopeToken string `json:"modelScopeToken"`
|
||||
}
|
||||
|
||||
type ModelItem struct {
|
||||
RepoID string `json:"repoID"`
|
||||
Name string `json:"name"`
|
||||
Downloads int64 `json:"downloads"`
|
||||
Likes int64 `json:"likes"`
|
||||
Size int64 `json:"size"`
|
||||
SizeFormatted string `json:"sizeFormatted"`
|
||||
}
|
||||
|
||||
type ModelFile struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
SizeFormatted string `json:"sizeFormatted"`
|
||||
}
|
||||
|
||||
type ModelInfo struct {
|
||||
ModelItem
|
||||
ModelCard string `json:"modelCard"`
|
||||
Files []ModelFile `json:"files"`
|
||||
}
|
||||
|
||||
type ModelDownloadTaskSearch struct {
|
||||
IncludeLocal bool `json:"includeLocal"`
|
||||
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type ModelDownloadTaskOperate struct {
|
||||
WithFile bool `json:"withFile"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type ModelDownloadTaskRetry struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type ModelDownloadTaskItem struct {
|
||||
LocalModel *LocalModelItem `json:"localModel,omitempty"`
|
||||
DownloadType string `json:"downloadType"`
|
||||
ID uint `json:"id"`
|
||||
Source string `json:"source"`
|
||||
RepoID string `json:"repoID"`
|
||||
Revision string `json:"revision"`
|
||||
ModelName string `json:"modelName"`
|
||||
TargetDir string `json:"targetDir"`
|
||||
Status string `json:"status"`
|
||||
Progress float64 `json:"progress"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
DownloadedSize int64 `json:"downloadedSize"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
CompletedAt string `json:"completedAt"`
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanRetry bool `json:"canRetry"`
|
||||
CanRemoveRecord bool `json:"canRemoveRecord"`
|
||||
}
|
||||
|
||||
type LocalModelSearch struct {
|
||||
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type LocalModelItem struct {
|
||||
DownloadType string `json:"downloadType"`
|
||||
RepoID string `json:"repoID"`
|
||||
Source string `json:"source"`
|
||||
Revision string `json:"revision"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
SizeFormatted string `json:"sizeFormatted"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type LocalModelDelete struct {
|
||||
WithFile bool `json:"withFile"`
|
||||
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type FeaturedModel struct {
|
||||
Key string `json:"key" yaml:"key"`
|
||||
Sort int `json:"sort" yaml:"sort"`
|
||||
Family string `json:"family" yaml:"category"`
|
||||
RepoID string `json:"repoID" yaml:"name"`
|
||||
Sources []string `json:"sources" yaml:"sources"`
|
||||
SourceModelIDs map[string]string `json:"sourceModelIds" yaml:"sourceModelIds"`
|
||||
Architecture string `json:"architecture" yaml:"modelType"`
|
||||
BackboneParams string `json:"backboneParams" yaml:"moeBackboneParameters"`
|
||||
ActiveParams string `json:"activeParams" yaml:"activeParameters"`
|
||||
Engram string `json:"engram" yaml:"engramParameters"`
|
||||
WeightGB float64 `json:"weightGB" yaml:"approximateWeightSizeGB"`
|
||||
Description map[string]string `json:"description" yaml:"description"`
|
||||
}
|
||||
|
||||
type FeaturedModelSearch struct {
|
||||
PageInfo
|
||||
Family string `json:"family"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type FeaturedModelPage struct {
|
||||
PageResult
|
||||
Families []string `json:"families"`
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package request
|
||||
|
||||
type MCPWebsiteBinding struct {
|
||||
Operation string `json:"operation"`
|
||||
RequestID string `json:"requestId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
Domain string `json:"domain"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
Upstream string `json:"upstream"`
|
||||
CoreHost string `json:"coreHost"`
|
||||
ProxyKey string `json:"proxyKey"`
|
||||
Operation string `json:"operation"`
|
||||
RequestID string `json:"requestId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
Domain string `json:"domain"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
Upstream string `json:"upstream"`
|
||||
CoreHost string `json:"coreHost"`
|
||||
ProxyKey string `json:"proxyKey"`
|
||||
DeleteWebsite bool `json:"deleteWebsite,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type TensorRTLLMSearch struct {
|
||||
}
|
||||
|
||||
type TensorRTLLMCreate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
ContainerName string `json:"containerName" validate:"required"`
|
||||
Version string `json:"version" validate:"required"`
|
||||
@@ -26,12 +27,14 @@ type TensorRTLLMUpdate struct {
|
||||
}
|
||||
|
||||
type TensorRTLLMDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type TensorRTLLMOperate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Operate string `json:"operate" validate:"required"`
|
||||
Operate string `json:"operate" validate:"required,oneof=start stop restart"`
|
||||
}
|
||||
|
||||
type DockerConfig struct {
|
||||
|
||||
@@ -51,6 +51,7 @@ type TagDTO struct {
|
||||
}
|
||||
|
||||
type AppInstalledCheck struct {
|
||||
TaskID string `json:"taskID"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
IsExist bool `json:"isExist"`
|
||||
@@ -107,6 +108,7 @@ type AppDetail struct {
|
||||
}
|
||||
|
||||
type AppInstallDTO struct {
|
||||
TaskID string `json:"taskID"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AppID uint `json:"appID"`
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package response
|
||||
|
||||
type MCPWebsiteBinding struct {
|
||||
State string `json:"state"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
RequestID string `json:"requestId"`
|
||||
Domain string `json:"domain"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
WebsiteStatus string `json:"websiteStatus"`
|
||||
Reason string `json:"reason"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Definitive bool `json:"definitive,omitempty"`
|
||||
State string `json:"state"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
RequestID string `json:"requestId"`
|
||||
Domain string `json:"domain"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
WebsiteStatus string `json:"websiteStatus"`
|
||||
Reason string `json:"reason"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Definitive bool `json:"definitive,omitempty"`
|
||||
WebsiteDeleted bool `json:"websiteDeleted,omitempty"`
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type TensorRTLLMsRes struct {
|
||||
}
|
||||
|
||||
type TensorRTLLMDTO struct {
|
||||
TaskID string `json:"taskID"`
|
||||
model.TensorRTLLM
|
||||
Version string `json:"version"`
|
||||
Model string `json:"model"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type AppInstall struct {
|
||||
Param string `json:"param"`
|
||||
Env string `json:"env"`
|
||||
DockerCompose string `json:"dockerCompose" `
|
||||
TaskID string `json:"taskID" gorm:"-"`
|
||||
Status string `json:"status" gorm:"not null"`
|
||||
Description string `json:"description"`
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ContainerImageCheckUpdate is a cache tied to the exact image used by a container.
|
||||
type ContainerImageCheckUpdate struct {
|
||||
ContainerID string `gorm:"primaryKey" json:"containerID"`
|
||||
ImageID string `json:"imageID"`
|
||||
ImageName string `json:"imageName"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
TargetImage string `json:"targetImage"`
|
||||
TargetImageID string `json:"-"`
|
||||
HasUpdate bool `gorm:"not null;default:false" json:"hasUpdate"`
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
CheckStatus string `json:"checkStatus"`
|
||||
CheckError string `json:"checkError"`
|
||||
}
|
||||
|
||||
// TableName preserves the table created by the original migration.
|
||||
func (ContainerImageCheckUpdate) TableName() string { return "container_image_updates" }
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type FeaturedModel struct {
|
||||
BaseModel
|
||||
Key string `json:"key"`
|
||||
Sort int `json:"sort"`
|
||||
Family string `json:"family"`
|
||||
RepoID string `json:"repoID"`
|
||||
Sources []string `json:"sources" gorm:"serializer:json"`
|
||||
SourceModelIDs map[string]string `json:"sourceModelIds" gorm:"serializer:json"`
|
||||
Architecture string `json:"architecture"`
|
||||
BackboneParams string `json:"backboneParams"`
|
||||
ActiveParams string `json:"activeParams"`
|
||||
Engram string `json:"engram"`
|
||||
WeightGB float64 `json:"weightGB"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ModelDownloadTask struct {
|
||||
DownloadType string `json:"downloadType"`
|
||||
BaseModel
|
||||
Source string `json:"source" gorm:"type:varchar(32);not null;index"`
|
||||
RepoID string `json:"repoID" gorm:"type:varchar(255);not null;index"`
|
||||
Revision string `json:"revision" gorm:"type:varchar(128)"`
|
||||
ModelName string `json:"modelName" gorm:"type:varchar(255);not null;index"`
|
||||
TargetDir string `json:"targetDir" gorm:"type:text"`
|
||||
Status string `json:"status" gorm:"type:varchar(32);not null;index"`
|
||||
Progress float64 `json:"progress"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
DownloadedSize int64 `json:"downloadedSize"`
|
||||
ErrorMessage string `json:"errorMessage" gorm:"type:text"`
|
||||
StartedAt *time.Time `json:"startedAt"`
|
||||
CompletedAt *time.Time `json:"completedAt"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -48,7 +48,7 @@ var catalog = map[string]Meta{
|
||||
},
|
||||
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||
"llmman": {
|
||||
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
||||
Key: "llmman", DisplayName: "llmman", Sort: 75, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
|
||||
@@ -18,8 +18,9 @@ type IAgentAccountRepo interface {
|
||||
Save(account *model.AgentAccount) error
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
Count() (int64, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -69,6 +70,12 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) Count() (int64, error) {
|
||||
var count int64
|
||||
err := getDb().Model(&model.AgentAccount{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||
normalizedProviders := normalizeProviders(providers)
|
||||
counts := make(map[string]int64, len(normalizedProviders))
|
||||
@@ -99,7 +106,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||
func (a AgentAccountRepo) SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -31,6 +31,7 @@ type IAppInstallRepo interface {
|
||||
GetFirst(opts ...DBOption) (model.AppInstall, error)
|
||||
Create(ctx context.Context, install *model.AppInstall) error
|
||||
Save(ctx context.Context, install *model.AppInstall) error
|
||||
UpdateStatus(install *model.AppInstall) error
|
||||
DeleteBy(opts ...DBOption) error
|
||||
Delete(ctx context.Context, install model.AppInstall) error
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
|
||||
@@ -45,6 +46,11 @@ func NewIAppInstallRepo() IAppInstallRepo {
|
||||
return &AppInstallRepo{}
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) UpdateStatus(install *model.AppInstall) error {
|
||||
return getDb().Model(&model.AppInstall{}).Where("id = ? AND updated_at = ?", install.ID, install.UpdatedAt).
|
||||
Updates(map[string]interface{}{"status": install.Status, "message": install.Message}).Error
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("app_detail_id in (?)", detailIds)
|
||||
|
||||
@@ -86,10 +86,10 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
|
||||
}
|
||||
if !withAll && len(quicks) == 0 {
|
||||
return []model.QuickJump{
|
||||
{Name: "ModelServices", Title: "aiTools.model.model", Recommend: 0, IsShow: true, Router: "/ai/model"},
|
||||
{Name: "Agent", Title: "aiTools.agents.agent", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"},
|
||||
{Name: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
|
||||
{Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"},
|
||||
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: false, Router: "/cronjobs"},
|
||||
{Name: "AppInstalled", Title: "home.appInstalled", Recommend: 70, IsShow: true, Router: "/apps/installed"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FeaturedModelRepo struct{}
|
||||
|
||||
type IFeaturedModelRepo interface {
|
||||
Page(page, size int, family, info string) (int64, []model.FeaturedModel, error)
|
||||
Families() ([]string, error)
|
||||
SyncAll(models []model.FeaturedModel, version string) error
|
||||
}
|
||||
|
||||
func NewIFeaturedModelRepo() IFeaturedModelRepo {
|
||||
return &FeaturedModelRepo{}
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) Page(page, size int, family, info string) (int64, []model.FeaturedModel, error) {
|
||||
var total int64
|
||||
var items []model.FeaturedModel
|
||||
db := global.DB.Model(&model.FeaturedModel{})
|
||||
if family != "" {
|
||||
db = db.Where("family = ?", family)
|
||||
}
|
||||
if info != "" {
|
||||
db = db.Where("LOWER(repo_id) LIKE ?", "%"+strings.ToLower(info)+"%")
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("sort asc").Order("id asc").Limit(size).Offset(size * (page - 1)).Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) Families() ([]string, error) {
|
||||
families := make([]string, 0)
|
||||
err := global.DB.Model(&model.FeaturedModel{}).Distinct("family").Order("family asc").Pluck("family", &families).Error
|
||||
return families, err
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) SyncAll(models []model.FeaturedModel, version string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("1 = 1").Delete(&model.FeaturedModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&models).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("key = ?", "FeaturedModelsVersion").
|
||||
Assign(model.Setting{Value: version}).
|
||||
FirstOrCreate(&model.Setting{Key: "FeaturedModelsVersion"}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ModelDownloadTaskRepo struct{}
|
||||
|
||||
type IModelDownloadTaskRepo interface {
|
||||
Page(page, size int, info, status string, opts ...DBOption) (int64, []model.ModelDownloadTask, error)
|
||||
GetFirst(opts ...DBOption) (model.ModelDownloadTask, error)
|
||||
List(opts ...DBOption) ([]model.ModelDownloadTask, error)
|
||||
Create(task *model.ModelDownloadTask) error
|
||||
Update(id uint, values map[string]interface{}) error
|
||||
UpdateBy(values map[string]interface{}, opts ...DBOption) error
|
||||
DeleteBy(opts ...DBOption) error
|
||||
WithByDownloadType(downloadType string) DBOption
|
||||
WithBySource(source string) DBOption
|
||||
WithByRepoID(repoID string) DBOption
|
||||
WithByTargetDir(targetDir string) DBOption
|
||||
WithByStatusIn(status []string) DBOption
|
||||
}
|
||||
|
||||
func NewIModelDownloadTaskRepo() IModelDownloadTaskRepo {
|
||||
return &ModelDownloadTaskRepo{}
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) WithBySource(source string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("source = ?", source)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) WithByRepoID(repoID string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("repo_id = ?", repoID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) WithByTargetDir(targetDir string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("target_dir = ?", targetDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) WithByStatusIn(status []string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("status in ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) Page(page, size int, info, status string, opts ...DBOption) (int64, []model.ModelDownloadTask, error) {
|
||||
var (
|
||||
total int64
|
||||
list []model.ModelDownloadTask
|
||||
)
|
||||
db := global.DB.Model(&model.ModelDownloadTask{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if info != "" {
|
||||
db = db.Where("repo_id LIKE ? OR model_name LIKE ?", "%"+info+"%", "%"+info+"%")
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("created_at desc").Limit(size).Offset(size * (page - 1)).Find(&list).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, list, nil
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) GetFirst(opts ...DBOption) (model.ModelDownloadTask, error) {
|
||||
var item model.ModelDownloadTask
|
||||
db := global.DB.Model(&model.ModelDownloadTask{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) Create(task *model.ModelDownloadTask) error {
|
||||
return global.DB.Create(task).Error
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) List(opts ...DBOption) ([]model.ModelDownloadTask, error) {
|
||||
var items []model.ModelDownloadTask
|
||||
db := global.DB.Model(&model.ModelDownloadTask{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) Update(id uint, values map[string]interface{}) error {
|
||||
return global.DB.Model(&model.ModelDownloadTask{}).Where("id = ?", id).Updates(values).Error
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) UpdateBy(values map[string]interface{}, opts ...DBOption) error {
|
||||
db := global.DB.Model(&model.ModelDownloadTask{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Updates(values).Error
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) DeleteBy(opts ...DBOption) error {
|
||||
db := global.DB.Model(&model.ModelDownloadTask{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.ModelDownloadTask{}).Error
|
||||
}
|
||||
|
||||
func (m *ModelDownloadTaskRepo) WithByDownloadType(downloadType string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("download_type = ?", downloadType)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type TaskRepo struct {
|
||||
type ITaskRepo interface {
|
||||
Save(ctx context.Context, task *model.Task) error
|
||||
GetFirst(opts ...DBOption) (model.Task, error)
|
||||
GetExecutingTask(scope string, resourceID uint) (model.Task, error)
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
||||
Update(ctx context.Context, task *model.Task) error
|
||||
UpdateRunningTaskToFailed(opts ...DBOption) error
|
||||
@@ -98,6 +99,12 @@ func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (t TaskRepo) GetExecutingTask(scope string, resourceID uint) (model.Task, error) {
|
||||
var item model.Task
|
||||
err := getTaskDb(WithByType(scope), t.WithResourceID(resourceID), t.WithByStatus(constant.StatusExecuting)).Order("created_at DESC").Limit(1).Find(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (t TaskRepo) Page(page, size int, opts ...DBOption) (int64, []model.Task, error) {
|
||||
var tasks []model.Task
|
||||
db := getTaskDb(opts...).Model(&model.Task{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+101
-2
@@ -1105,9 +1105,23 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Models != nil {
|
||||
if account.AppInstallID != 0 {
|
||||
return buserr.WithDetail("ErrInvalidParams", "local model pool is managed by the deployment", nil)
|
||||
}
|
||||
models, err = normalizeAgentAccountModels(account, req.Models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureAccountModelsNotBound(account, models); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
requestedVerifyModel := req.VerifyModel
|
||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
}
|
||||
}
|
||||
verifyModel := ""
|
||||
if account.AppInstallID == 0 || len(models) > 0 {
|
||||
@@ -1131,7 +1145,16 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
if req.Models != nil {
|
||||
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||
for _, item := range models {
|
||||
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||
}
|
||||
err = agentAccountRepo.SaveWithModels(account, rows)
|
||||
} else {
|
||||
err = agentAccountRepo.Save(account)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
@@ -1275,6 +1298,31 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
|
||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
if account.Provider == "ollama" {
|
||||
if account.AppInstallID != 0 {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key != "ollama" || !install.SyncModelAccount {
|
||||
return buserr.New("ErrAgentAccountUnavailable")
|
||||
}
|
||||
localModels, err := aiRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range localModels {
|
||||
if sameProviderModelID("ollama", item.Name, nextModel.ID) {
|
||||
return (&AIToolService{}).Recreate(dto.OllamaModelName{Name: item.Name, TaskID: req.TaskID})
|
||||
}
|
||||
}
|
||||
return (&AIToolService{}).Create(dto.OllamaModelName{Name: nextModel.ID, TaskID: req.TaskID})
|
||||
}
|
||||
nextModel.ID, err = resolveExistingOllamaModel(account, nextModel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sortOrder := len(models) + 1
|
||||
record := &model.AgentAccountModel{
|
||||
AccountID: account.ID,
|
||||
@@ -1305,6 +1353,15 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Provider == "ollama" && !sameProviderModelID("ollama", record.Model, nextModel.ID) {
|
||||
if account.AppInstallID != 0 {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
nextModel.ID, err = resolveExistingOllamaModel(account, nextModel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
nextModel.RecordID = req.Model.RecordID
|
||||
for _, item := range models {
|
||||
if item.RecordID == req.Model.RecordID {
|
||||
@@ -1343,6 +1400,10 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1368,6 +1429,44 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
||||
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.DeleteFiles && account.Provider != "ollama" {
|
||||
return buserr.New("ErrAgentProviderNotSupported")
|
||||
}
|
||||
if req.DeleteFiles {
|
||||
if account.AppInstallID != 0 {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key != "ollama" {
|
||||
return buserr.New("ErrAgentAccountUnavailable")
|
||||
}
|
||||
localModels, err := aiRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localIDs := make([]uint, 0)
|
||||
for _, item := range localModels {
|
||||
if !sameProviderModelID("ollama", item.Name, record.Model) {
|
||||
continue
|
||||
}
|
||||
if item.Status == constant.StatusWaiting {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
localIDs = append(localIDs, item.ID)
|
||||
}
|
||||
if err := cmd.NewCommandMgr().Run("docker", "exec", install.ContainerName, "ollama", "rm", record.Model); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(localIDs) > 0 {
|
||||
if err := aiRepo.Delete(repo.WithByIDs(localIDs)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if err := deleteRemoteOllamaModel(account, record.Model); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||
"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"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
@@ -88,7 +94,7 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
|
||||
}
|
||||
}
|
||||
if modelsChanged {
|
||||
err = agentAccountRepo.SaveLocal(account, rows)
|
||||
err = agentAccountRepo.SaveWithModels(account, rows)
|
||||
} else if accountChanged {
|
||||
err = agentAccountRepo.Save(account)
|
||||
} else {
|
||||
@@ -136,6 +142,62 @@ func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBase
|
||||
return err
|
||||
}
|
||||
|
||||
func updateOllamaModelPool(modelName string, remove bool) error {
|
||||
install, err := appInstallRepo.LoadInstallAppByKeyAndName("ollama", "")
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !install.SyncModelAccount {
|
||||
return nil
|
||||
}
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
models, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if remove {
|
||||
for _, item := range models {
|
||||
if !sameProviderModelID("ollama", item.ID, modelName) {
|
||||
continue
|
||||
}
|
||||
if err := agentAccountModelRepo.DeleteByID(item.RecordID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if sameProviderModelID("ollama", account.VerifyModel, modelName) {
|
||||
account.VerifyModel = ""
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := compactPersistedAgentAccountModelSortOrder(account.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, exists := findAgentAccountModelForProvider("ollama", models, modelName); exists {
|
||||
return nil
|
||||
}
|
||||
record := &model.AgentAccountModel{AccountID: account.ID, Model: modelName, Name: modelName, SortOrder: len(models) + 1}
|
||||
if err := agentAccountModelRepo.Create(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||
addressType := install.ModelAccountBaseURLType
|
||||
if addressType == "" {
|
||||
@@ -200,3 +262,39 @@ func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, cont
|
||||
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRemoteOllamaModel(account *model.AgentAccount, modelName string) error {
|
||||
body, err := json.Marshal(map[string]string{"model": modelName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
baseURL := strings.TrimSuffix(strings.TrimRight(account.BaseURL, "/"), "/v1")
|
||||
req, err := http.NewRequest(http.MethodDelete, baseURL+"/api/delete", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+account.APIKey)
|
||||
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("delete Ollama model failed: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveExistingOllamaModel(account *model.AgentAccount, modelID string) (string, error) {
|
||||
available, err := providercatalog.DiscoverModels(account.BaseURL, account.APIKey)
|
||||
if err != nil {
|
||||
return "", buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||
}
|
||||
for _, id := range available {
|
||||
if sameProviderModelID("ollama", id, modelID) {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
return "", buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
@@ -1328,7 +1328,7 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
||||
if account == nil {
|
||||
return nil, fmt.Errorf("account is required")
|
||||
}
|
||||
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
|
||||
if (account.Provider == "vllm" || account.Provider == "llmman") && len(requested) > 1 {
|
||||
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
||||
}
|
||||
if len(requested) > 0 {
|
||||
@@ -1517,6 +1517,14 @@ func sameProviderModelID(provider, left, right string) bool {
|
||||
}
|
||||
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
|
||||
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
|
||||
if provider == "ollama" {
|
||||
if leftComparable != "" && !strings.Contains(path.Base(leftComparable), ":") {
|
||||
leftComparable += ":latest"
|
||||
}
|
||||
if rightComparable != "" && !strings.Contains(path.Base(rightComparable), ":") {
|
||||
rightComparable += ":latest"
|
||||
}
|
||||
}
|
||||
return leftComparable != "" && leftComparable == rightComparable
|
||||
}
|
||||
|
||||
|
||||
+24
-18
@@ -117,6 +117,7 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, info.ID)
|
||||
if err != nil {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
global.LOG.Errorf("new task for exec shell failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -126,19 +127,17 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||
itemSize, err := loadModelSize(info.Name, containerName)
|
||||
if len(itemSize) != 0 {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
||||
} else {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error {
|
||||
return updateOllamaModelPool(info.Name, false)
|
||||
}, nil)
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -167,6 +166,9 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
if modelInfo.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if modelInfo.Status == constant.StatusWaiting {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
containerName, err := LoadContainerName()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -176,6 +178,7 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, modelInfo.ID)
|
||||
if err != nil {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
global.LOG.Errorf("new task for exec shell failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -185,19 +188,17 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||
itemSize, err := loadModelSize(modelInfo.Name, containerName)
|
||||
if len(itemSize) != 0 {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
||||
} else {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error {
|
||||
return updateOllamaModelPool(modelInfo.Name, false)
|
||||
}, nil)
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -219,11 +220,16 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
return fmt.Errorf("handle ollama rm %s failed, %v", item.Name, err)
|
||||
}
|
||||
}
|
||||
_ = aiRepo.Delete(repo.WithByID(item.ID))
|
||||
if err := aiRepo.Delete(repo.WithByID(item.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updateOllamaModelPool(item.Name, true); err != nil {
|
||||
return err
|
||||
}
|
||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||
_ = os.Remove(logItem)
|
||||
}
|
||||
return SyncOllamaModelAccount(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||
|
||||
@@ -642,7 +642,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
}
|
||||
|
||||
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour)
|
||||
installTimeout := time.Hour
|
||||
if app.Key == "ollama" || app.Key == "vllm" {
|
||||
installTimeout = 0
|
||||
}
|
||||
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, installTimeout)
|
||||
|
||||
go func() {
|
||||
if taskErr := installTask.Execute(); taskErr != nil {
|
||||
|
||||
@@ -223,6 +223,79 @@ func TestExplicitConfigComposeDetectsCustomAndReusesCurrentCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovedCurrentComposeDoesNotBlockUpgrade(t *testing.T) {
|
||||
for _, custom := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("custom=%v", custom), func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/demo/1.0/docker-compose.yml" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
version := strings.Split(r.URL.Path, "/")[2]
|
||||
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
db, _, install, details := composeUpgradeDB(t, server.URL)
|
||||
if err := db.Model(&details[1]).UpdateColumn("status", constant.AppTakeDown).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if custom {
|
||||
install.DockerCompose += " environment:\n USER_SETTING: preserve\n"
|
||||
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
service := NewIAppInstalledService()
|
||||
config, err := service.GetParams(install.ID)
|
||||
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || !config.IsEdit {
|
||||
t.Fatalf("removed template must preserve the instance and warn about custom configuration: %+v, %v", config, err)
|
||||
}
|
||||
var current model.AppDetail
|
||||
if err := db.First(¤t, details[1].ID).Error; err != nil || current.DockerCompose != "" {
|
||||
t.Fatalf("instance Compose was cached as the official template: %+v, %v", current, err)
|
||||
}
|
||||
noCompose := false
|
||||
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
|
||||
if err != nil || len(versions) != 2 {
|
||||
t.Fatalf("removed current version blocked available versions: %+v, %v", versions, err)
|
||||
}
|
||||
for _, version := range versions {
|
||||
previews, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: version.Version})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, preview := range previews {
|
||||
if preview.Version == version.Version && !strings.Contains(preview.DockerCompose, "demo:"+version.Version) {
|
||||
t.Fatalf("upgrade did not use the selected new template: %+v", preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
var stored model.AppInstall
|
||||
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Version != install.Version || stored.Status != install.Status {
|
||||
t.Fatalf("upgrade preview modified the running instance: %+v, %v", stored, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovedCurrentComposeRejectsInvalidInstanceCompose(t *testing.T) {
|
||||
for _, compose := range []string{"", "services: []", "services: ["} {
|
||||
t.Run(compose, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
db, _, install, _ := composeUpgradeDB(t, server.URL)
|
||||
if err := db.Model(&install).UpdateColumn("docker_compose", compose).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config, err := NewIAppInstalledService().GetParams(install.ID); err == nil || config != nil {
|
||||
t.Fatalf("invalid instance Compose was accepted: %+v, %v", config, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
@@ -123,7 +122,10 @@ func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []respo
|
||||
}
|
||||
}
|
||||
|
||||
installDTOs, _ := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate)
|
||||
installDTOs, err := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if req.Update {
|
||||
total = int64(len(installDTOs))
|
||||
}
|
||||
@@ -168,6 +170,7 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
||||
res.Version = appInstall.Version
|
||||
res.CreatedAt = appInstall.CreatedAt
|
||||
res.Status = appInstall.Status
|
||||
res.TaskID = appInstall.TaskID
|
||||
res.AppInstallID = appInstall.ID
|
||||
res.IsExist = true
|
||||
res.InstallPath = path.Join(global.Dir.AppInstallDir, appInstall.App.Key, appInstall.Name)
|
||||
@@ -256,6 +259,21 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||
if req.Operate != constant.Upgrade {
|
||||
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer appUpgradeLocks.Delete(install.ID)
|
||||
}
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, install.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if operationTask.ID != "" && req.Operate != constant.Sync {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
}
|
||||
if install.Status == constant.StatusRestarting && (req.Operate == constant.Stop || req.Operate == constant.Delete) && !req.ForceDelete {
|
||||
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||
if err != nil {
|
||||
@@ -269,8 +287,8 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return buserr.New("ErrInstallDirNotFound")
|
||||
}
|
||||
dockerComposePath := install.GetComposePath()
|
||||
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
||||
return operateAppWithLifecycleScripts(install, req, nil)
|
||||
if (req.UseLifecycleScripts || install.App.Key == "ollama" || install.App.Key == "vllm") && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
||||
return operateAppTask(install, req, nil)
|
||||
}
|
||||
switch req.Operate {
|
||||
case constant.Rebuild:
|
||||
@@ -303,10 +321,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
TaskID: req.TaskID,
|
||||
UseLifecycleScripts: req.UseLifecycleScripts,
|
||||
}
|
||||
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return deleteAppInstall(deleteReq)
|
||||
case constant.Sync:
|
||||
return syncAppInstallStatus(&install, true)
|
||||
case constant.Upgrade:
|
||||
@@ -334,20 +349,25 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
}
|
||||
}
|
||||
|
||||
func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
|
||||
taskType := task.TaskUpdate
|
||||
func operateAppTask(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
|
||||
var taskType string
|
||||
switch req.Operate {
|
||||
case constant.Start:
|
||||
taskType = "Run"
|
||||
install.Status = constant.StatusStarting
|
||||
case constant.Restart:
|
||||
taskType = task.TaskRestart
|
||||
install.Status = constant.StatusRestarting
|
||||
case constant.Stop:
|
||||
taskType = "Stop"
|
||||
install.Status = constant.StatusWaiting
|
||||
default:
|
||||
return errors.New("lifecycle script operation not supported")
|
||||
}
|
||||
install.Message = ""
|
||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||
if err != nil {
|
||||
@@ -358,19 +378,31 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
||||
}
|
||||
return err
|
||||
}
|
||||
ready := make(chan error, 1)
|
||||
started := make(chan struct{})
|
||||
operation := string(req.Operate)
|
||||
operationTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
||||
func(t *task.Task) error {
|
||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
}
|
||||
close(started)
|
||||
ready <- nil
|
||||
if err := runScript(t, &install, operation); err != nil {
|
||||
return err
|
||||
if req.UseLifecycleScripts {
|
||||
if err := runScript(t, &install, operation); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var out string
|
||||
var err error
|
||||
switch req.Operate {
|
||||
case constant.Start:
|
||||
out, err = compose.Up(install.GetComposePath())
|
||||
case constant.Stop:
|
||||
out, err = compose.Stop(install.GetComposePath())
|
||||
case constant.Restart:
|
||||
out, err = compose.Restart(install.GetComposePath())
|
||||
}
|
||||
if strings.TrimSpace(out) != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.Operate == constant.Stop {
|
||||
install.Status = constant.StatusStopped
|
||||
@@ -391,26 +423,22 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
||||
},
|
||||
nil,
|
||||
0,
|
||||
time.Hour,
|
||||
0,
|
||||
)
|
||||
go func() {
|
||||
if taskErr := operationTask.Execute(); taskErr != nil {
|
||||
select {
|
||||
case <-started:
|
||||
default:
|
||||
ready <- taskErr
|
||||
return
|
||||
}
|
||||
if onFailure != nil {
|
||||
onFailure(taskErr)
|
||||
return
|
||||
}
|
||||
install.Status = constant.StatusUpErr
|
||||
install.Message = taskErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
taskErr := operationTask.Execute()
|
||||
if taskErr == nil {
|
||||
return
|
||||
}
|
||||
if onFailure != nil {
|
||||
onFailure(taskErr)
|
||||
return
|
||||
}
|
||||
install.Status = constant.StatusUpErr
|
||||
install.Message = taskErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}()
|
||||
return <-ready
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||
@@ -439,6 +467,19 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if installed.App.Key == "ollama" || installed.App.Key == "vllm" {
|
||||
if _, loaded := appUpgradeLocks.LoadOrStore(installed.ID, struct{}{}); loaded {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer appUpgradeLocks.Delete(installed.ID)
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, installed.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if operationTask.ID != "" {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
}
|
||||
oldInstalled := installed
|
||||
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
|
||||
if ok {
|
||||
@@ -578,7 +619,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||
}
|
||||
if req.UseLifecycleScripts {
|
||||
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
|
||||
err = operateAppTask(installed, request.AppInstalledOperate{
|
||||
InstallId: installed.ID,
|
||||
Operate: constant.Restart,
|
||||
TaskID: req.TaskID,
|
||||
@@ -591,10 +632,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
restoreConfig(err)
|
||||
return err
|
||||
}
|
||||
if !req.UseLifecycleScripts {
|
||||
installed.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||
}
|
||||
|
||||
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
||||
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
||||
@@ -1062,7 +1099,17 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
rawCompose := install.DockerCompose
|
||||
if install.App.Key != vllmAppKeyForUpgrade {
|
||||
if err := ensureAppDetailCompose(install.App, &detail); err != nil {
|
||||
return nil, err
|
||||
var businessErr buserr.BusinessError
|
||||
if !errors.As(err, &businessErr) || businessErr.Msg != "ErrAppVersionUnavailable" {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAppDetailCompose([]byte(install.DockerCompose)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// A removed template cannot prove the instance is unmodified.
|
||||
res.RawCompose = install.DockerCompose
|
||||
res.IsEdit = true
|
||||
return &res, nil
|
||||
}
|
||||
rawCompose, err = getUpgradeCompose(install, detail)
|
||||
if err != nil {
|
||||
@@ -1079,41 +1126,56 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
}
|
||||
|
||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
operation := ""
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling:
|
||||
operation = task.TaskInstall
|
||||
case constant.StatusUpgrading:
|
||||
operation = task.TaskUpgrade
|
||||
case constant.StatusRebuilding:
|
||||
operation = task.TaskBuild
|
||||
case constant.StatusUninstalling:
|
||||
operation = task.TaskUninstall
|
||||
case constant.StatusStarting, constant.StatusWaiting:
|
||||
operation = task.TaskUpdate
|
||||
case constant.StatusRestarting:
|
||||
operation = task.TaskRestart
|
||||
}
|
||||
if operation != "" {
|
||||
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusWaiting:
|
||||
return nil
|
||||
case constant.StatusRestarting:
|
||||
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, appInstall.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
appInstall.TaskID = operationTask.ID
|
||||
if operationTask.ID != "" {
|
||||
appInstall.Status = localEngineTaskStatus(operationTask.Operate, appInstall.Status)
|
||||
return nil
|
||||
}
|
||||
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||
if !force {
|
||||
force = true
|
||||
} else {
|
||||
operation := ""
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling:
|
||||
operation = task.TaskInstall
|
||||
case constant.StatusUpgrading:
|
||||
operation = task.TaskUpgrade
|
||||
case constant.StatusRebuilding:
|
||||
operation = task.TaskBuild
|
||||
case constant.StatusUninstalling:
|
||||
operation = task.TaskUninstall
|
||||
case constant.StatusStarting:
|
||||
operation = "Run"
|
||||
case constant.StatusWaiting:
|
||||
operation = "Stop"
|
||||
case constant.StatusRestarting:
|
||||
operation = task.TaskRestart
|
||||
}
|
||||
if operation != "" {
|
||||
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusWaiting:
|
||||
return nil
|
||||
case constant.StatusRestarting:
|
||||
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||
if !force {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewClient()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -74,3 +75,29 @@ func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func localEngineTaskStatus(operation, status string) string {
|
||||
switch operation {
|
||||
case task.TaskInstall, task.TaskCreate:
|
||||
return constant.StatusInstalling
|
||||
case task.TaskUpgrade:
|
||||
return constant.StatusUpgrading
|
||||
case task.TaskUninstall, task.TaskDelete:
|
||||
return constant.StatusUninstalling
|
||||
case task.TaskBuild:
|
||||
return constant.StatusRebuilding
|
||||
case task.TaskRestart:
|
||||
return constant.StatusRestarting
|
||||
case "Run":
|
||||
return constant.StatusStarting
|
||||
case "Stop":
|
||||
return constant.StatusWaiting
|
||||
case task.TaskUpdate:
|
||||
if status == constant.StatusWaiting {
|
||||
return constant.StatusWaiting
|
||||
}
|
||||
return constant.StatusStarting
|
||||
default:
|
||||
return constant.StatusWaiting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +92,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if install.Status == constant.StatusUpgrading {
|
||||
if install.Status == constant.StatusUpgrading && install.App.Key != "ollama" && install.App.Key != "vllm" {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
@@ -107,6 +104,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
appUpgradeLocks.Delete(install.ID)
|
||||
}
|
||||
}()
|
||||
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feishuLockPath := ""
|
||||
if install.App.Key == constant.AppOpenclaw {
|
||||
@@ -185,7 +185,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if ctx.rollbackErr != nil {
|
||||
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
|
||||
upgradeTask.Task.ErrorMsg = taskErr.Error()
|
||||
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
|
||||
_ = taskRepo.Update(context.Background(), upgradeTask.Task)
|
||||
}
|
||||
if !ctx.stopAttempted || ctx.rollbackErr == nil {
|
||||
restored := ctx.original
|
||||
|
||||
@@ -498,7 +498,11 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
|
||||
uninstallTimeout := 30 * time.Minute
|
||||
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||
uninstallTimeout = 0
|
||||
}
|
||||
uninstallTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil, 0, uninstallTimeout)
|
||||
go func() {
|
||||
if err := uninstallTask.Execute(); err != nil {
|
||||
install.Status = constant.StatusError
|
||||
@@ -1226,28 +1230,36 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecyc
|
||||
|
||||
func rebuildApp(appInstall model.AppInstall) error {
|
||||
appInstall.Status = constant.StatusRebuilding
|
||||
_ = appInstallRepo.Save(context.Background(), &appInstall)
|
||||
go func() {
|
||||
dockerComposePath := appInstall.GetComposePath()
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err := appInstallRepo.Save(context.Background(), &appInstall); err != nil {
|
||||
return err
|
||||
}
|
||||
buildTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskBuild, task.TaskScopeApp, "", appInstall.ID)
|
||||
if err != nil {
|
||||
return handleErr(appInstall, err, "")
|
||||
}
|
||||
buildTask.AddSubTaskWithOps(buildTask.Name, func(t *task.Task) error {
|
||||
out, err := compose.Down(appInstall.GetComposePath())
|
||||
t.Log(out)
|
||||
if err != nil {
|
||||
_ = handleErr(appInstall, err, out)
|
||||
return
|
||||
return handleErr(appInstall, err, out)
|
||||
}
|
||||
out, err = compose.Up(appInstall.GetComposePath())
|
||||
t.Log(out)
|
||||
if err != nil {
|
||||
_ = handleErr(appInstall, err, out)
|
||||
return
|
||||
return handleErr(appInstall, err, out)
|
||||
}
|
||||
containerNames, err := getContainerNames(appInstall)
|
||||
if err != nil {
|
||||
_ = handleErr(appInstall, err, out)
|
||||
return
|
||||
return handleErr(appInstall, err, out)
|
||||
}
|
||||
appInstall.ContainerName = strings.Join(containerNames, ",")
|
||||
|
||||
appInstall.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &appInstall)
|
||||
return appInstallRepo.Save(context.Background(), &appInstall)
|
||||
}, nil, 0, 0)
|
||||
go func() {
|
||||
if err := buildTask.Execute(); err != nil {
|
||||
_ = handleErr(appInstall, err, "")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -1477,7 +1489,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
||||
}
|
||||
appInstall.Status = constant.StatusError
|
||||
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||
} else {
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
}
|
||||
return
|
||||
}
|
||||
notFoundNames := make([]string, 0)
|
||||
@@ -1536,7 +1552,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
||||
appInstall.Message = msg
|
||||
appInstall.Status = constant.StatusUnHealthy
|
||||
}
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||
} else {
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) {
|
||||
@@ -1544,22 +1564,43 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
res []response.AppInstallDTO
|
||||
containersMap map[string]container.Summary
|
||||
)
|
||||
if sync {
|
||||
syncContainers := sync
|
||||
for _, installed := range appInstallList {
|
||||
if installed.App.Key == "ollama" || installed.App.Key == "vllm" {
|
||||
syncContainers = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if syncContainers {
|
||||
cli, err := docker.NewClient()
|
||||
if err == nil {
|
||||
defer cli.Close()
|
||||
containers, err := cli.ListAllContainers()
|
||||
if err == nil {
|
||||
containersMap = make(map[string]container.Summary, len(containers))
|
||||
for _, contain := range containers {
|
||||
containersMap[contain.Names[0]] = contain
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
containers, err := cli.ListAllContainers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containersMap = make(map[string]container.Summary, len(containers))
|
||||
for _, contain := range containers {
|
||||
containersMap[contain.Names[0]] = contain
|
||||
}
|
||||
}
|
||||
|
||||
for _, installed := range appInstallList {
|
||||
if sync && appInstallOperationPending(installed.Status) {
|
||||
localEngine := installed.App.Key == "ollama" || installed.App.Key == "vllm"
|
||||
if localEngine {
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, installed.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
installed.TaskID = operationTask.ID
|
||||
if operationTask.ID != "" {
|
||||
installed.Status = localEngineTaskStatus(operationTask.Operate, installed.Status)
|
||||
} else {
|
||||
synAppInstall(containersMap, &installed, true)
|
||||
}
|
||||
} else if sync && appInstallOperationPending(installed.Status) {
|
||||
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1568,7 +1609,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
continue
|
||||
}
|
||||
|
||||
if sync && !doNotNeedSync(installed) {
|
||||
if sync && !localEngine && !doNotNeedSync(installed) {
|
||||
synAppInstall(containersMap, &installed, false)
|
||||
}
|
||||
|
||||
@@ -1582,6 +1623,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
AppDetailID: installed.AppDetailId,
|
||||
Version: installed.Version,
|
||||
Status: installed.Status,
|
||||
TaskID: installed.TaskID,
|
||||
Message: installed.Message,
|
||||
HttpPort: installed.HttpPort,
|
||||
HttpsPort: installed.HttpsPort,
|
||||
|
||||
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
|
||||
backData = records[start:end]
|
||||
}
|
||||
|
||||
// Batch-load persisted checks; never contact Docker Hub while rendering the list.
|
||||
ids := make([]string, 0, len(backData))
|
||||
for _, row := range backData {
|
||||
ids = append(ids, row.ContainerID)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
var checks []model.ContainerImageCheckUpdate
|
||||
if err := global.DB.Where("container_id IN ?", ids).Find(&checks).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
byID := make(map[string]model.ContainerImageCheckUpdate, len(checks))
|
||||
for _, check := range checks {
|
||||
byID[check.ContainerID] = check
|
||||
}
|
||||
for i := range backData {
|
||||
check, ok := byID[backData[i].ContainerID]
|
||||
if ok && sameContainerImage(check.ImageID, backData[i].ImageId) && check.ImageName == backData[i].ImageName {
|
||||
backData[i].LatestVersion = check.LatestVersion
|
||||
backData[i].TargetImage = check.TargetImage
|
||||
backData[i].HasUpdate = check.HasUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(backData); i++ {
|
||||
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
|
||||
if install.ID > 0 {
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/dockerhub"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const imageCheckUpdateSetting = "ContainerImageUpdateEnabled"
|
||||
|
||||
type ImageCheckUpdateStatus struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Running bool `json:"running"`
|
||||
Checked int `json:"checked"`
|
||||
Updates int `json:"updates"`
|
||||
Failed int `json:"failed"`
|
||||
Skipped int `json:"skipped"`
|
||||
Error string `json:"error"`
|
||||
FinishedAt int64 `json:"finishedAt"`
|
||||
}
|
||||
|
||||
var imageCheckUpdateState struct {
|
||||
sync.Mutex
|
||||
status ImageCheckUpdateStatus
|
||||
}
|
||||
|
||||
func GetContainerImageCheckUpdateStatus() (ImageCheckUpdateStatus, error) {
|
||||
var setting model.Setting
|
||||
if err := global.DB.Where("key = ?", imageCheckUpdateSetting).First(&setting).Error; err != nil {
|
||||
return ImageCheckUpdateStatus{}, err
|
||||
}
|
||||
imageCheckUpdateState.Lock()
|
||||
defer imageCheckUpdateState.Unlock()
|
||||
status := imageCheckUpdateState.status
|
||||
status.Enabled = setting.Value == "Enable"
|
||||
return status, nil
|
||||
}
|
||||
func SetContainerImageCheckUpdateSchedule(enabled bool) error {
|
||||
value := "Disable"
|
||||
if enabled {
|
||||
_, err := req_helper.LoadRuntimeProxyURL()
|
||||
if errors.Is(err, req_helper.ErrProxyRequired) {
|
||||
return buserr.New("ErrImageCheckProxyRequired")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value = "Enable"
|
||||
}
|
||||
return global.DB.Model(&model.Setting{}).Where("key = ?", imageCheckUpdateSetting).Update("value", value).Error
|
||||
}
|
||||
|
||||
// StartContainerImageCheckUpdate coalesces manual and scheduled runs on this node.
|
||||
func StartContainerImageCheckUpdate() (string, error) {
|
||||
imageCheckUpdateState.Lock()
|
||||
defer imageCheckUpdateState.Unlock()
|
||||
if imageCheckUpdateState.status.Running {
|
||||
return imageCheckUpdateState.status.TaskID, nil
|
||||
}
|
||||
taskItem, err := task.NewTask(i18n.GetMsgByKey("ContainerImageCheckUpdate"), task.TaskScan, task.TaskScopeImage, "", 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Persist before returning the ID so the log drawer can immediately find it.
|
||||
if err := global.TaskDB.Save(taskItem.Task).Error; err != nil {
|
||||
_ = taskItem.Logger.Out.(io.Closer).Close()
|
||||
taskItem.DeleteLogFile()
|
||||
if cancel, ok := global.LoadTaskCancel(taskItem.TaskID); ok {
|
||||
cancel()
|
||||
}
|
||||
global.RemoveTaskCancel(taskItem.TaskID)
|
||||
return "", err
|
||||
}
|
||||
imageCheckUpdateState.status = ImageCheckUpdateStatus{Running: true, TaskID: taskItem.TaskID}
|
||||
go func() {
|
||||
result := ImageCheckUpdateStatus{TaskID: taskItem.TaskID}
|
||||
defer func() {
|
||||
result.FinishedAt = time.Now().UnixMilli()
|
||||
imageCheckUpdateState.Lock()
|
||||
imageCheckUpdateState.status = result
|
||||
imageCheckUpdateState.Unlock()
|
||||
}()
|
||||
// The action owns its deadline so the task runner waits for cleanup and logs.
|
||||
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerImageCheckUpdate"), func(t *task.Task) (err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("image update check failed: %v", recovered)
|
||||
}
|
||||
t.Log(i18n.GetMsgWithMap("ContainerImageCheckSummary", map[string]interface{}{
|
||||
"checked": result.Checked, "updates": result.Updates, "failed": result.Failed, "skipped": result.Skipped,
|
||||
}))
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(t.TaskCtx, 30*time.Minute)
|
||||
defer cancel()
|
||||
if err = runContainerImageCheckUpdate(ctx, &result, t.Log); err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Failed > 0 {
|
||||
return fmt.Errorf("%s", i18n.GetMsgWithMap("ContainerImageCheckFailures", map[string]interface{}{"count": result.Failed}))
|
||||
}
|
||||
return nil
|
||||
}, nil, 0, 0)
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
result.Error = err.Error()
|
||||
global.LOG.Errorf("container image check: %s", err)
|
||||
}
|
||||
}()
|
||||
return taskItem.TaskID, nil
|
||||
}
|
||||
|
||||
// These caches live only for one check. Remote results do not depend on a
|
||||
// container's local image ID, which is compared separately for every baseline.
|
||||
type imageCheckUpdateRepository struct {
|
||||
client imageCheckUpdateHubClient
|
||||
err error
|
||||
tags []string
|
||||
tagsLoaded bool
|
||||
tagsErr error
|
||||
}
|
||||
type imageCheckUpdateTargetKey struct {
|
||||
repository, tag string
|
||||
platform dockerhub.Platform
|
||||
}
|
||||
type imageCheckUpdateTargetResult struct {
|
||||
target dockerhub.Target
|
||||
compatible bool
|
||||
err error
|
||||
}
|
||||
type imageCheckUpdateResult struct {
|
||||
record model.ContainerImageCheckUpdate
|
||||
err error
|
||||
}
|
||||
type imageCheckUpdateCache struct {
|
||||
username, password string
|
||||
httpClient *http.Client
|
||||
repositories map[string]*imageCheckUpdateRepository
|
||||
targets map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult
|
||||
}
|
||||
|
||||
func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error {
|
||||
httpClient, err := req_helper.NewRuntimeHTTPClient()
|
||||
if errors.Is(err, req_helper.ErrProxyRequired) {
|
||||
return buserr.New("ErrImageCheckProxyRequired")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if httpClient.Transport != nil {
|
||||
defer httpClient.CloseIdleConnections()
|
||||
}
|
||||
log := func(key string, values map[string]interface{}) {
|
||||
if logger != nil {
|
||||
logger(i18n.GetMsgWithMap(key, values))
|
||||
}
|
||||
}
|
||||
cli, err := newImageCheckUpdateDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log("ContainerImageCheckScope", map[string]interface{}{"count": len(containers)})
|
||||
// Load old results once, rather than issuing one SELECT for every failed check.
|
||||
var previousRows []model.ContainerImageCheckUpdate
|
||||
if err := global.DB.WithContext(ctx).Find(&previousRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
previous := make(map[string]model.ContainerImageCheckUpdate, len(previousRows))
|
||||
for _, row := range previousRows {
|
||||
previous[row.ContainerID] = row
|
||||
}
|
||||
cache := imageCheckUpdateCache{httpClient: httpClient, repositories: map[string]*imageCheckUpdateRepository{}, targets: map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult{}}
|
||||
cfg := config.LoadDefaultConfigFile(io.Discard)
|
||||
if cfg != nil {
|
||||
for _, host := range []string{"https://index.docker.io/v1/", "docker.io", "registry-1.docker.io"} {
|
||||
auth, e := cfg.GetAuthConfig(host)
|
||||
if e == nil && auth.Username != "" {
|
||||
cache.username, cache.password = auth.Username, auth.Password
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
checks := map[string]imageCheckUpdateResult{}
|
||||
ids := make([]string, 0, len(containers))
|
||||
names := make(map[string]string, len(containers))
|
||||
pending := make([]model.ContainerImageCheckUpdate, 0, 100)
|
||||
for _, item := range containers {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
ids = append(ids, item.ID)
|
||||
name := item.ID
|
||||
if len(item.Names) > 0 {
|
||||
name = strings.TrimPrefix(item.Names[0], "/")
|
||||
}
|
||||
names[item.ID] = name
|
||||
log("ContainerImageCheckItem", map[string]interface{}{"name": name, "image": item.Image})
|
||||
repository, tag, supported := dockerhub.Reference(item.Image)
|
||||
key := repository + ":" + tag + "@" + item.ImageID
|
||||
entry, found := checks[key]
|
||||
if !found {
|
||||
entry.record, entry.err = checkContainerImageUpdate(ctx, cli, item, repository, tag, supported, &cache)
|
||||
checks[key] = entry
|
||||
}
|
||||
record := entry.record
|
||||
record.ContainerID, record.ImageName = item.ID, item.Image
|
||||
if entry.err != nil {
|
||||
record.CheckStatus, record.CheckError = "failed", entry.err.Error()
|
||||
old := previous[item.ID]
|
||||
if old.ImageID == item.ImageID && old.ImageName == item.Image {
|
||||
record.LatestVersion, record.TargetImage, record.TargetImageID, record.HasUpdate = old.LatestVersion, old.TargetImage, old.TargetImageID, old.HasUpdate
|
||||
}
|
||||
}
|
||||
pending = append(pending, record)
|
||||
if len(pending) == 100 {
|
||||
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
|
||||
return err
|
||||
}
|
||||
pending = pending[:0]
|
||||
}
|
||||
}
|
||||
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
|
||||
return err
|
||||
}
|
||||
query := global.DB.WithContext(ctx).Where("1 = 1")
|
||||
if len(ids) > 0 {
|
||||
query = query.Where("container_id NOT IN ?", ids)
|
||||
}
|
||||
return query.Delete(&model.ContainerImageCheckUpdate{}).Error
|
||||
}
|
||||
|
||||
func checkContainerImageUpdate(ctx context.Context, cli imageCheckUpdateDockerClient, item container.Summary, repository, tag string, supported bool, cache *imageCheckUpdateCache) (model.ContainerImageCheckUpdate, error) {
|
||||
record := model.ContainerImageCheckUpdate{ImageID: item.ImageID, ImageName: item.Image, CheckStatus: "success", CheckedAt: time.Now()}
|
||||
if !supported {
|
||||
record.CheckStatus = "skipped"
|
||||
return record, nil
|
||||
}
|
||||
inspected, err := cli.ImageInspect(ctx, item.ImageID)
|
||||
if err != nil {
|
||||
return record, err
|
||||
}
|
||||
repo, loaded := cache.repositories[repository]
|
||||
if !loaded {
|
||||
repo = &imageCheckUpdateRepository{}
|
||||
repo.client, repo.err = newImageCheckUpdateHubClient(ctx, repository, cache.username, cache.password, cache.httpClient)
|
||||
cache.repositories[repository] = repo
|
||||
}
|
||||
if repo.err != nil {
|
||||
return record, repo.err
|
||||
}
|
||||
if dockerhub.NeedsTags(tag) {
|
||||
if !repo.tagsLoaded {
|
||||
repo.tags, repo.tagsErr = repo.client.Tags(ctx)
|
||||
repo.tagsLoaded = true
|
||||
}
|
||||
if repo.tagsErr != nil {
|
||||
return record, repo.tagsErr
|
||||
}
|
||||
}
|
||||
platform := dockerhub.Platform{OS: inspected.Os, Architecture: inspected.Architecture, Variant: inspected.Variant}
|
||||
for _, candidate := range dockerhub.Candidates(tag, repo.tags) {
|
||||
key := imageCheckUpdateTargetKey{repository: repository, tag: candidate, platform: platform}
|
||||
remote, loaded := cache.targets[key]
|
||||
if !loaded {
|
||||
remote.target, remote.compatible, remote.err = repo.client.Resolve(ctx, candidate, platform)
|
||||
cache.targets[key] = remote
|
||||
}
|
||||
if remote.err != nil {
|
||||
return record, remote.err
|
||||
}
|
||||
if !remote.compatible {
|
||||
continue
|
||||
}
|
||||
record.LatestVersion = remote.target.Tag
|
||||
record.TargetImage = "docker.io/" + repository + ":" + remote.target.Tag
|
||||
record.TargetImageID = remote.target.ConfigDigest
|
||||
// Both versioned and floating tags can be republished with new image content.
|
||||
record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID
|
||||
return record, nil
|
||||
}
|
||||
return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture)
|
||||
}
|
||||
|
||||
// Revalidate immediately before each small write batch. Never hold a database
|
||||
// transaction open while waiting for Docker or Docker Hub.
|
||||
func saveContainerImageCheckUpdates(ctx context.Context, cli imageCheckUpdateDockerClient, pending []model.ContainerImageCheckUpdate, names map[string]string, result *ImageCheckUpdateStatus, log func(string, map[string]interface{})) error {
|
||||
records := make([]model.ContainerImageCheckUpdate, 0, len(pending))
|
||||
for _, record := range pending {
|
||||
current, err := cli.ContainerInspect(ctx, record.ContainerID)
|
||||
if err != nil && !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
if err != nil || current.Image != record.ImageID {
|
||||
result.Skipped++
|
||||
log("ContainerImageCheckChanged", map[string]interface{}{"name": names[record.ContainerID]})
|
||||
continue
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := global.DB.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&records, 100).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Count and announce only results that were successfully persisted.
|
||||
for _, record := range records {
|
||||
values := map[string]interface{}{"name": names[record.ContainerID], "image": record.ImageName}
|
||||
switch record.CheckStatus {
|
||||
case "failed":
|
||||
result.Failed++
|
||||
values["error"] = record.CheckError
|
||||
log("ContainerImageCheckFailed", values)
|
||||
case "skipped":
|
||||
result.Skipped++
|
||||
log("ContainerImageCheckSkipped", values)
|
||||
default:
|
||||
result.Checked++
|
||||
if record.HasUpdate {
|
||||
result.Updates++
|
||||
values["target"] = record.TargetImage
|
||||
log("ContainerImageCheckAvailable", values)
|
||||
} else {
|
||||
log("ContainerImageCheckCurrent", values)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameContainerImage guards cached results after external image changes.
|
||||
func sameContainerImage(stored, displayed string) bool {
|
||||
return strings.TrimPrefix(stored, "sha256:") == strings.TrimPrefix(displayed, "sha256:")
|
||||
}
|
||||
|
||||
// Reconcile only after Docker confirms the replacement container uses the target.
|
||||
func reconcileContainerImageCheckUpdate(oldID, newID, imageID, imageName string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var previous model.ContainerImageCheckUpdate
|
||||
if err := tx.Where("container_id = ?", oldID).Find(&previous).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if previous.ContainerID == "" {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Where("container_id = ?", oldID).Delete(&model.ContainerImageCheckUpdate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if previous.TargetImageID != imageID {
|
||||
return nil
|
||||
}
|
||||
previous.ContainerID = newID
|
||||
previous.ImageID = imageID
|
||||
previous.ImageName = imageName
|
||||
previous.HasUpdate = false
|
||||
previous.CheckStatus = "success"
|
||||
previous.CheckError = ""
|
||||
previous.CheckedAt = time.Now()
|
||||
return tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(&previous).Error
|
||||
})
|
||||
}
|
||||
|
||||
type imageCheckUpdateDockerClient interface {
|
||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||
ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error)
|
||||
Close() error
|
||||
}
|
||||
type imageCheckUpdateHubClient interface {
|
||||
Tags(context.Context) ([]string, error)
|
||||
Resolve(context.Context, string, dockerhub.Platform) (dockerhub.Target, bool, error)
|
||||
}
|
||||
|
||||
var newImageCheckUpdateDockerClient = func() (imageCheckUpdateDockerClient, error) { return docker.NewDockerClient() }
|
||||
var newImageCheckUpdateHubClient = func(ctx context.Context, repository, username, password string, httpClient *http.Client) (imageCheckUpdateHubClient, error) {
|
||||
return dockerhub.New(ctx, repository, username, password, httpClient)
|
||||
}
|
||||
@@ -163,6 +163,11 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
||||
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||
return upgradeErr
|
||||
}
|
||||
if updated, inspectErr := client.ContainerInspect(ctx, item); inspectErr == nil {
|
||||
if cacheErr := reconcileContainerImageCheckUpdate(oldContainer.ID, updated.ID, updated.Image, updated.Config.Image); cacheErr != nil {
|
||||
global.LOG.Warnf("refresh container image update state: %v", cacheErr)
|
||||
}
|
||||
}
|
||||
if cleanupErr != nil {
|
||||
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
|
||||
}
|
||||
@@ -307,7 +312,6 @@ func switchContainer(
|
||||
return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false)
|
||||
}
|
||||
|
||||
|
||||
const (
|
||||
containerUpgradePullTimeout = 30 * time.Minute
|
||||
containerUpgradeReplaceTimeout = 15 * time.Minute
|
||||
|
||||
@@ -414,7 +414,7 @@ func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
if showCount == 0 {
|
||||
return buserr.New("ErrMinQuickJump")
|
||||
}
|
||||
if showCount > 4 {
|
||||
if showCount > 5 {
|
||||
return buserr.New("ErrMaxQuickJump")
|
||||
}
|
||||
|
||||
@@ -709,6 +709,12 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
switch quicks[i].Name {
|
||||
case "ModelServices":
|
||||
count, err := agentAccountRepo.Count()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load model service count: %v", err)
|
||||
}
|
||||
quicks[i].Detail = fmt.Sprintf("%d", count)
|
||||
case "File":
|
||||
count, err := launcherRepo.CountQuickDirectories()
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,9 @@ package service
|
||||
import "github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
var (
|
||||
vmRepo = repo.NewIVMRepo()
|
||||
modelDownloadTaskRepo = repo.NewIModelDownloadTaskRepo()
|
||||
featuredModelRepo = repo.NewIFeaturedModelRepo()
|
||||
appRepo = repo.NewIAppRepo()
|
||||
appTagRepo = repo.NewIAppTagRepo()
|
||||
appDetailRepo = repo.NewIAppDetailRepo()
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var featuredModelsSyncMu sync.Mutex
|
||||
|
||||
func (m *ModelDownloaderService) PageFeaturedModels(req dto.FeaturedModelSearch) (*dto.FeaturedModelPage, error) {
|
||||
total, models, err := featuredModelRepo.Page(req.Page, req.PageSize, strings.TrimSpace(req.Family), strings.TrimSpace(req.Info))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.FeaturedModel, 0, len(models))
|
||||
for _, item := range models {
|
||||
var description map[string]string
|
||||
if err := json.Unmarshal([]byte(item.Description), &description); err != nil {
|
||||
return nil, fmt.Errorf("parse featured model %s description failed: %w", item.Key, err)
|
||||
}
|
||||
items = append(items, dto.FeaturedModel{
|
||||
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
|
||||
SourceModelIDs: item.SourceModelIDs,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
families, err := featuredModelRepo.Families()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.FeaturedModelPage{PageResult: dto.PageResult{Total: total, Items: items}, Families: families}, nil
|
||||
}
|
||||
|
||||
func (m *ModelDownloaderService) SyncFeaturedModels() error {
|
||||
if global.CONF.Base.IsOffline {
|
||||
return nil
|
||||
}
|
||||
featuredModelsSyncMu.Lock()
|
||||
defer featuredModelsSyncMu.Unlock()
|
||||
localVersion, err := settingRepo.GetValueByKey("FeaturedModelsVersion")
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
data, version, err := req_helper.LoadAIResource("featured-models", localVersion)
|
||||
if err != nil || data == nil {
|
||||
return err
|
||||
}
|
||||
var remote struct {
|
||||
Models []dto.FeaturedModel `yaml:"models"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &remote); err != nil {
|
||||
return fmt.Errorf("parse featured models failed: %w", err)
|
||||
}
|
||||
if len(remote.Models) == 0 {
|
||||
return errors.New("featured models data is empty")
|
||||
}
|
||||
keys := make(map[string]bool, len(remote.Models))
|
||||
models := make([]model.FeaturedModel, 0, len(remote.Models))
|
||||
for i := range remote.Models {
|
||||
item := &remote.Models[i]
|
||||
item.Key = strings.TrimSpace(item.Key)
|
||||
item.Family = strings.TrimSpace(item.Family)
|
||||
item.RepoID = strings.TrimSpace(item.RepoID)
|
||||
if item.Key == "" || item.Family == "" || item.RepoID == "" || (item.Architecture != "MoE" && item.Architecture != "Dense") || item.WeightGB < 0 {
|
||||
return fmt.Errorf("invalid featured model: %s", item.Key)
|
||||
}
|
||||
if keys[item.Key] {
|
||||
return fmt.Errorf("duplicate featured model key: %s", item.Key)
|
||||
}
|
||||
keys[item.Key] = true
|
||||
if len(item.Sources) == 0 {
|
||||
item.Sources = []string{"modelscope"}
|
||||
}
|
||||
for _, source := range item.Sources {
|
||||
if source != "modelscope" && source != "huggingface" {
|
||||
return fmt.Errorf("invalid featured model %s source: %s", item.Key, source)
|
||||
}
|
||||
}
|
||||
description, err := json.Marshal(item.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
models = append(models, model.FeaturedModel{
|
||||
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
|
||||
SourceModelIDs: item.SourceModelIDs,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
|
||||
Description: string(description),
|
||||
})
|
||||
}
|
||||
return featuredModelRepo.SyncAll(models, version)
|
||||
}
|
||||
|
||||
func SyncFeaturedModels() {
|
||||
if global.CONF.Base.IsOffline {
|
||||
return
|
||||
}
|
||||
status, err := settingRepo.GetValueByKey("FeaturedModelsSync")
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
global.LOG.Errorf("load featured model sync setting failed: %v", err)
|
||||
return
|
||||
}
|
||||
if err == nil && status != constant.StatusEnable {
|
||||
return
|
||||
}
|
||||
if err := NewIModelDownloaderService().SyncFeaturedModels(); err != nil {
|
||||
global.LOG.Errorf("sync featured models failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/dockerhub"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@@ -35,6 +37,7 @@ import (
|
||||
type ImageService struct{}
|
||||
|
||||
type IImageService interface {
|
||||
SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error)
|
||||
Page(req dto.PageImage) (int64, interface{}, error)
|
||||
List() ([]dto.Options, error)
|
||||
ListAll() ([]dto.ImageInfo, error)
|
||||
@@ -560,3 +563,17 @@ func checkUsed(imageID string, containers []container.Summary) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *ImageService) SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error) {
|
||||
client, err := req_helper.NewRuntimeHTTPClient()
|
||||
if err != nil {
|
||||
if errors.Is(err, req_helper.ErrProxyRequired) {
|
||||
return dockerhub.SearchResult{Items: []dockerhub.SearchItem{}, ProxyRequired: true}, nil
|
||||
}
|
||||
return dockerhub.SearchResult{}, err
|
||||
}
|
||||
if client.Transport != nil {
|
||||
defer client.CloseIdleConnections()
|
||||
}
|
||||
return dockerhub.Search(ctx, client, req.Query, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
@@ -17,14 +17,17 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"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/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -52,6 +55,7 @@ func init() {
|
||||
|
||||
type MCPWebsiteBindingService struct {
|
||||
createWebsite func(request.WebsiteCreate) error
|
||||
deleteWebsite func(request.WebsiteDelete) error
|
||||
updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error
|
||||
applyInclude func(model.Website) error
|
||||
removeInclude func(model.Website) error
|
||||
@@ -63,6 +67,7 @@ type MCPWebsiteBindingService struct {
|
||||
func NewMCPWebsiteBindingService() *MCPWebsiteBindingService {
|
||||
s := &MCPWebsiteBindingService{
|
||||
createWebsite: func(req request.WebsiteCreate) error { return NewIWebsiteService().CreateWebsite(req) },
|
||||
deleteWebsite: func(req request.WebsiteDelete) error { return NewIWebsiteService().DeleteWebsite(req) },
|
||||
updateHTTPS: func(ctx context.Context, req request.WebsiteHTTPSOp) error {
|
||||
if req.Enable {
|
||||
current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID)
|
||||
@@ -140,14 +145,22 @@ func mcpWebsiteHasWWWMount(mounts []container.MountPoint, root string) bool {
|
||||
}
|
||||
|
||||
type mcpWebsiteOwner struct {
|
||||
BindingID string `json:"bindingId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
KeyHash string `json:"keyHash"`
|
||||
BindHash string `json:"bindHash"`
|
||||
ProxyHash string `json:"proxyHash"`
|
||||
RequestID string `json:"requestId"`
|
||||
RequestHash string `json:"requestHash"`
|
||||
Operation string `json:"operation"`
|
||||
BindingID string `json:"bindingId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
KeyHash string `json:"keyHash"`
|
||||
BindHash string `json:"bindHash"`
|
||||
ProxyHash string `json:"proxyHash"`
|
||||
RequestID string `json:"requestId"`
|
||||
RequestHash string `json:"requestHash"`
|
||||
Operation string `json:"operation"`
|
||||
DeleteRoot string `json:"deleteRoot,omitempty"`
|
||||
DeletePaths []mcpWebsiteDeletePath `json:"deletePaths,omitempty"`
|
||||
}
|
||||
|
||||
type mcpWebsiteDeletePath struct {
|
||||
Kind string `json:"kind"`
|
||||
Device uint64 `json:"device"`
|
||||
Inode uint64 `json:"inode"`
|
||||
}
|
||||
|
||||
func mcpWebsiteHash(value string) string {
|
||||
@@ -236,6 +249,9 @@ func mcpWebsiteFailure(reason string, definitive, conflict bool) response.MCPWeb
|
||||
if conflict {
|
||||
code, message = "MCP_WEBSITE_CONFLICT", "MCP website configuration changed"
|
||||
}
|
||||
if definitive && reason == "http_port_requires_default" {
|
||||
code, message = "MCP_WEBSITE_HTTP_PORT_REQUIRED", "Use the OpenResty HTTP port before enabling HTTPS"
|
||||
}
|
||||
return response.MCPWebsiteBinding{State: "unavailable", Reason: reason, Code: code, Error: message, Definitive: definitive}
|
||||
}
|
||||
|
||||
@@ -461,13 +477,190 @@ func mcpWebsiteReadProxy(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func mcpWebsitePayloadHash(req request.MCPWebsiteBinding) string {
|
||||
return mcpWebsiteHash(fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID))
|
||||
payload := fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID)
|
||||
if req.DeleteWebsite {
|
||||
payload += "\ndeleteWebsite=true"
|
||||
}
|
||||
return mcpWebsiteHash(payload)
|
||||
}
|
||||
|
||||
func mcpWebsiteBindHash(req request.MCPWebsiteBinding) string {
|
||||
return mcpWebsiteHash(fmt.Sprintf("%s\n%t\n%d", req.Domain, req.EnableSSL, req.SSLID))
|
||||
}
|
||||
|
||||
func mcpWebsiteDeleteLocations(site model.Website) map[string]string {
|
||||
return map[string]string{"site": GetSitePath(site, SiteDir), "config": GetSitePath(site, SiteConf)}
|
||||
}
|
||||
|
||||
func mcpWebsiteDeleteParents() error {
|
||||
root := filepath.Clean(GetWebSiteRootDir())
|
||||
if !filepath.IsAbs(root) {
|
||||
return errors.New("invalid managed website root")
|
||||
}
|
||||
for _, directory := range []string{root, filepath.Join(root, "sites"), filepath.Join(root, "conf.d")} {
|
||||
info, err := os.Lstat(directory)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return errors.New("managed website directory changed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mcpWebsiteDeleteIdentity(kind, location string) (mcpWebsiteDeletePath, bool, error) {
|
||||
info, err := os.Lstat(location)
|
||||
if os.IsNotExist(err) {
|
||||
return mcpWebsiteDeletePath{}, false, nil
|
||||
}
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || kind == "site" && !info.IsDir() || kind == "config" && !info.Mode().IsRegular() {
|
||||
return mcpWebsiteDeletePath{}, false, errors.New("managed website path changed")
|
||||
}
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return mcpWebsiteDeletePath{}, false, errors.New("managed website path identity is unavailable")
|
||||
}
|
||||
return mcpWebsiteDeletePath{Kind: kind, Device: uint64(stat.Dev), Inode: uint64(stat.Ino)}, true, nil
|
||||
}
|
||||
|
||||
func mcpWebsiteRecordDeletePaths(site model.Website, owner *mcpWebsiteOwner) error {
|
||||
if err := mcpWebsiteDeleteParents(); err != nil {
|
||||
return err
|
||||
}
|
||||
owner.DeleteRoot, owner.DeletePaths = filepath.Clean(GetWebSiteRootDir()), nil
|
||||
for kind, location := range mcpWebsiteDeleteLocations(site) {
|
||||
identity, exists, err := mcpWebsiteDeleteIdentity(kind, location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
owner.DeletePaths = append(owner.DeletePaths, identity)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mcpWebsiteVerifyDeletePaths(site model.Website, owner mcpWebsiteOwner) (map[string]string, error) {
|
||||
if owner.DeleteRoot == "" || owner.DeleteRoot != filepath.Clean(GetWebSiteRootDir()) {
|
||||
return nil, errors.New("managed website root changed")
|
||||
}
|
||||
if err := mcpWebsiteDeleteParents(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expected := make(map[string]mcpWebsiteDeletePath)
|
||||
for _, identity := range owner.DeletePaths {
|
||||
if identity.Kind != "site" && identity.Kind != "config" || expected[identity.Kind].Kind != "" {
|
||||
return nil, errors.New("invalid managed website deletion journal")
|
||||
}
|
||||
expected[identity.Kind] = identity
|
||||
}
|
||||
remaining := make(map[string]string)
|
||||
for kind, location := range mcpWebsiteDeleteLocations(site) {
|
||||
actual, exists, err := mcpWebsiteDeleteIdentity(kind, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
if expected[kind] != actual {
|
||||
return nil, errors.New("managed website path was replaced")
|
||||
}
|
||||
remaining[kind] = location
|
||||
}
|
||||
}
|
||||
return remaining, nil
|
||||
}
|
||||
|
||||
func mcpWebsiteCleanupDeletedPrivate(site model.Website) error {
|
||||
if err := mcpWebsitePrivateDirectory(site, false); err != nil {
|
||||
if _, statErr := os.Lstat(filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName))); os.IsNotExist(statErr) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) {
|
||||
return errors.New("managed proxy remains after website deletion")
|
||||
}
|
||||
if err := os.Remove(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
directory := filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName))
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return os.Remove(directory)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MCPWebsiteBindingService) deletedWebsite(req request.MCPWebsiteBinding, site model.Website, owner mcpWebsiteOwner, recoverFiles bool) response.MCPWebsiteBinding {
|
||||
if replacement, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err == nil && replacement.ID == site.ID {
|
||||
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
|
||||
}
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
remaining, err := mcpWebsiteVerifyDeletePaths(site, owner)
|
||||
if err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", false, true)
|
||||
}
|
||||
if len(remaining) > 0 && !recoverFiles {
|
||||
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
|
||||
}
|
||||
for kind, location := range remaining {
|
||||
fileOp := files.NewFileOp()
|
||||
if kind == "site" {
|
||||
err = fileOp.DeleteDir(location)
|
||||
} else {
|
||||
err = fileOp.DeleteFile(location)
|
||||
}
|
||||
if err != nil {
|
||||
return mcpWebsiteFailure("website_delete_failed", false, false)
|
||||
}
|
||||
}
|
||||
remaining, err = mcpWebsiteVerifyDeletePaths(site, owner)
|
||||
if err != nil || len(remaining) != 0 {
|
||||
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
|
||||
}
|
||||
if err = mcpWebsiteCleanupDeletedPrivate(site); err != nil {
|
||||
return mcpWebsiteFailure("ownership_cleanup_failed", false, false)
|
||||
}
|
||||
return response.MCPWebsiteBinding{State: "unbound", WebsiteID: site.ID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true}
|
||||
}
|
||||
|
||||
func (s *MCPWebsiteBindingService) missingDeletedWebsite(req request.MCPWebsiteBinding) response.MCPWebsiteBinding {
|
||||
site := model.Website{BaseModel: model.BaseModel{ID: req.WebsiteID}, Alias: mcpWebsiteAlias(req.BindingID)}
|
||||
if _, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
owner, ownerErr := mcpWebsiteReadOwner(site)
|
||||
if ownerErr == nil && owner.BindingID == req.BindingID && (req.WebsiteID == 0 || owner.WebsiteID == req.WebsiteID) && owner.KeyHash == mcpWebsiteHash(req.ProxyKey) &&
|
||||
owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == mcpWebsitePayloadHash(req) {
|
||||
site.ID = owner.WebsiteID
|
||||
if _, err := websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
return s.deletedWebsite(req, site, owner, true)
|
||||
}
|
||||
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); !os.IsNotExist(err) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) {
|
||||
return mcpWebsiteFailure("proxy_modified", false, true)
|
||||
}
|
||||
if err := mcpWebsiteDeleteParents(); err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", false, true)
|
||||
}
|
||||
for _, location := range mcpWebsiteDeleteLocations(site) {
|
||||
if _, err := os.Lstat(location); !os.IsNotExist(err) {
|
||||
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
|
||||
}
|
||||
}
|
||||
return response.MCPWebsiteBinding{State: "unbound", WebsiteID: req.WebsiteID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true}
|
||||
}
|
||||
|
||||
func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBinding, site model.Website) bool {
|
||||
host, port, err := mcpWebsiteDomain(req.Domain)
|
||||
if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID {
|
||||
@@ -479,12 +672,9 @@ func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBind
|
||||
return false
|
||||
}
|
||||
port = installed.HttpPort
|
||||
if req.EnableSSL {
|
||||
port = installed.HttpsPort
|
||||
}
|
||||
}
|
||||
for _, domain := range site.Domains {
|
||||
if domain.Domain == host && domain.Port == port {
|
||||
if domain.Domain == host && domain.Port == port && !domain.SSL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -617,6 +807,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
if req.Operation != "status" && req.Operation != "bind" && req.Operation != "update" && req.Operation != "unbind" && req.Operation != "sync" {
|
||||
return mcpWebsiteFailure("invalid_operation", true, false)
|
||||
}
|
||||
if req.DeleteWebsite && req.Operation != "unbind" {
|
||||
return mcpWebsiteFailure("invalid_operation", true, false)
|
||||
}
|
||||
if req.BindingID == "" && req.WebsiteID == 0 && req.Operation == "status" {
|
||||
return response.MCPWebsiteBinding{State: "unbound"}
|
||||
}
|
||||
@@ -639,6 +832,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
site, err := s.load(req)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if req.Operation == "status" || req.Operation == "unbind" {
|
||||
if req.DeleteWebsite {
|
||||
return s.missingDeletedWebsite(req)
|
||||
}
|
||||
if req.WebsiteID == 0 || req.Operation == "unbind" {
|
||||
return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID}
|
||||
}
|
||||
@@ -658,19 +854,31 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
domain, port, _ := mcpWebsiteDomain(req.Domain)
|
||||
if port == 0 {
|
||||
port = installed.HttpPort
|
||||
if req.EnableSSL {
|
||||
port = installed.HttpsPort
|
||||
}
|
||||
}
|
||||
if existing, conflictErr := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain), websiteDomainRepo.WithPort(port)); conflictErr == nil && existing.ID > 0 {
|
||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||
} else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) {
|
||||
if req.EnableSSL && port != installed.HttpPort {
|
||||
return mcpWebsiteFailure("http_port_requires_default", true, false)
|
||||
}
|
||||
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port}}}
|
||||
if req.EnableSSL {
|
||||
candidate.Protocol = constant.ProtocolHTTPS
|
||||
}
|
||||
if conflictErr := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); conflictErr != nil {
|
||||
var business buserr.BusinessError
|
||||
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||
}
|
||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||
}
|
||||
create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID,
|
||||
Domains: []request.WebsiteDomain{{Domain: domain, Port: port, SSL: req.EnableSSL}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}}
|
||||
Domains: []request.WebsiteDomain{{Domain: domain, Port: port}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}}
|
||||
previousMutation := mutationStarted
|
||||
mutationStarted = true
|
||||
if createErr := s.createWebsite(create); createErr != nil {
|
||||
var business buserr.BusinessError
|
||||
if errors.As(createErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||
mutationStarted = previousMutation
|
||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||
}
|
||||
return mcpWebsiteFailure("website_create_failed", false, false)
|
||||
}
|
||||
site, err = s.load(req)
|
||||
@@ -701,6 +909,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||
}
|
||||
if ownerErr != nil {
|
||||
if req.DeleteWebsite {
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", true, true)
|
||||
}
|
||||
@@ -711,15 +922,44 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
} else if owner.BindingID != req.BindingID || owner.WebsiteID != site.ID || owner.KeyHash != mcpWebsiteHash(req.ProxyKey) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", true, true)
|
||||
}
|
||||
if req.DeleteWebsite {
|
||||
if site.Type != constant.Static || site.AppInstallID != 0 || site.RuntimeID != 0 || site.DbID != 0 || site.DbType != "" || site.ParentWebsiteID != 0 {
|
||||
return mcpWebsiteFailure("website_modified", true, true)
|
||||
}
|
||||
if err = mcpWebsiteDeleteParents(); err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", true, true)
|
||||
}
|
||||
for kind, location := range mcpWebsiteDeleteLocations(site) {
|
||||
if _, _, err = mcpWebsiteDeleteIdentity(kind, location); err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", true, true)
|
||||
}
|
||||
}
|
||||
children, childErr := websiteRepo.GetBy(websiteRepo.WithParentID(site.ID))
|
||||
if childErr != nil {
|
||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||
}
|
||||
if len(children) != 0 {
|
||||
return mcpWebsiteFailure("website_modified", true, true)
|
||||
}
|
||||
}
|
||||
if req.Operation == "status" {
|
||||
return s.status(req, site, owner)
|
||||
}
|
||||
operationHash := mcpWebsitePayloadHash(req)
|
||||
deleteRetry := req.DeleteWebsite && owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == operationHash && owner.DeleteRoot != ""
|
||||
if owner.DeleteRoot != "" && req.Operation != "status" && (owner.RequestID != req.RequestID || owner.RequestHash != operationHash) {
|
||||
return mcpWebsiteFailure("operation_request_changed", false, true)
|
||||
}
|
||||
if deleteRetry {
|
||||
if _, err = mcpWebsiteVerifyDeletePaths(site, owner); err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", false, true)
|
||||
}
|
||||
}
|
||||
if owner.RequestID == req.RequestID && req.RequestID != "" {
|
||||
if owner.RequestHash != operationHash {
|
||||
return mcpWebsiteFailure("operation_request_changed", true, true)
|
||||
}
|
||||
if owner.Operation == "unbind" || owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) {
|
||||
if owner.Operation == "unbind" && !req.DeleteWebsite || owner.Operation != "unbind" && owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) {
|
||||
return s.status(req, site, owner)
|
||||
}
|
||||
}
|
||||
@@ -774,17 +1014,48 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
return mcpWebsiteFailure("proxy_remove_failed", false, false)
|
||||
}
|
||||
}
|
||||
if req.DeleteWebsite && !deleteRetry {
|
||||
if err = mcpWebsiteRecordDeletePaths(site, &owner); err != nil {
|
||||
return mcpWebsiteFailure("website_directory_changed", false, true)
|
||||
}
|
||||
}
|
||||
owner.Operation, owner.RequestID, owner.RequestHash = "unbind", req.RequestID, operationHash
|
||||
mutationStarted = true
|
||||
if err = mcpWebsiteWriteOwner(site, owner); err != nil {
|
||||
return mcpWebsiteFailure("ownership_write_failed", false, false)
|
||||
}
|
||||
if req.DeleteWebsite {
|
||||
if err = s.deleteWebsite(request.WebsiteDelete{ID: site.ID}); err != nil {
|
||||
return mcpWebsiteFailure("website_delete_failed", false, false)
|
||||
}
|
||||
if _, err = websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
|
||||
}
|
||||
return s.deletedWebsite(req, site, owner, false)
|
||||
}
|
||||
return s.status(req, site, owner)
|
||||
}
|
||||
if req.Operation == "update" {
|
||||
if req.Domain != site.PrimaryDomain {
|
||||
return mcpWebsiteFailure("domain_changed", true, true)
|
||||
}
|
||||
if req.EnableSSL {
|
||||
installed, installErr := s.openresty()
|
||||
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||
}
|
||||
host, _, _ := mcpWebsiteDomain(site.PrimaryDomain)
|
||||
usesHTTPPort := false
|
||||
for _, domain := range site.Domains {
|
||||
if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL {
|
||||
usesHTTPPort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !usesHTTPPort {
|
||||
return mcpWebsiteFailure("http_port_requires_default", true, false)
|
||||
}
|
||||
}
|
||||
if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID {
|
||||
mutationStarted = true
|
||||
if err = s.updateHTTPS(context.WithoutCancel(ctx), request.WebsiteHTTPSOp{WebsiteID: site.ID, Enable: req.EnableSSL, Type: constant.SSLExisted, WebsiteSSLID: req.SSLID, HttpConfig: constant.HTTPSOnly}); err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
const tokenizerDownloadMaxSize = 256 << 20
|
||||
|
||||
type tokenizerDownloadMetadata struct {
|
||||
Source string `json:"source"`
|
||||
RepoID string `json:"repoID"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
|
||||
func filterTokenizerDownloadFiles(items []dto.ModelFile) []dto.ModelFile {
|
||||
result := make([]dto.ModelFile, 0)
|
||||
for _, item := range items {
|
||||
name := item.Name
|
||||
if !filepath.IsLocal(name) || strings.ContainsAny(name, "\\\x00\r\n") || filepath.ToSlash(filepath.Clean(name)) != name {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(name, "/") {
|
||||
if strings.HasPrefix(name, "chat_templates/") && strings.HasSuffix(name, ".jinja") {
|
||||
result = append(result, item)
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch name {
|
||||
case "config.json", "tokenizer_config.json", "tokenizer.json", "special_tokens_map.json", "added_tokens.json", "vocab.json", "vocab.txt", "merges.txt", "tekken.json":
|
||||
result = append(result, item)
|
||||
default:
|
||||
if strings.HasSuffix(name, ".model") || strings.HasPrefix(name, "tokenizer.model") || strings.HasSuffix(name, ".tiktoken") || strings.HasSuffix(name, ".py") || strings.HasSuffix(name, ".jinja") {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validateTokenizerDownloadFiles(items []dto.ModelFile) error {
|
||||
if len(items) == 0 || len(items) > 512 {
|
||||
return newModelDownloaderInputError("tokenizer must contain between 1 and 512 files")
|
||||
}
|
||||
names := make(map[string]bool, len(items))
|
||||
var size int64
|
||||
vocabulary := false
|
||||
for _, item := range items {
|
||||
if names[item.Name] || item.Size < 0 || item.Size > tokenizerDownloadMaxSize-size {
|
||||
return newModelDownloaderInputError("invalid tokenizer files or total size exceeds 256 MiB")
|
||||
}
|
||||
size += item.Size
|
||||
names[item.Name] = true
|
||||
if item.Name == "tokenizer.json" || item.Name == "tekken.json" || item.Name == "vocab.txt" || strings.HasSuffix(item.Name, ".model") || strings.HasPrefix(item.Name, "tokenizer.model") || strings.HasSuffix(item.Name, ".tiktoken") {
|
||||
vocabulary = true
|
||||
}
|
||||
}
|
||||
if !vocabulary && !(names["vocab.json"] && names["merges.txt"]) {
|
||||
return newModelDownloaderInputError("repository has no supported tokenizer vocabulary in its root directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDownloadedTokenizer(root string) error {
|
||||
var items []dto.ModelFile
|
||||
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
stat, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !stat.Mode().IsRegular() || stat.Size() > tokenizerDownloadMaxSize {
|
||||
return newModelDownloaderInputError("invalid tokenizer file")
|
||||
}
|
||||
name, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, dto.ModelFile{Name: filepath.ToSlash(name), Size: stat.Size()})
|
||||
if strings.HasSuffix(name, ".json") {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !json.Valid(content) {
|
||||
return newModelDownloaderInputError("invalid tokenizer JSON: %s", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateTokenizerDownloadFiles(items)
|
||||
}
|
||||
|
||||
func listDownloadedTokenizers(info string) ([]dto.LocalModelItem, error) {
|
||||
items := make([]dto.LocalModelItem, 0)
|
||||
root := filepath.Join(global.CONF.Base.InstallDir, "1panel/ai/tokenizer")
|
||||
directories, err := listModelDownloadDirectories(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range directories {
|
||||
if _, err := normalizeModelDownloaderRepoID(item.Name); err != nil {
|
||||
continue
|
||||
}
|
||||
path, err := filepath.EvalSymlinks(item.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := os.ReadFile(filepath.Join(path, ".1panel-tokenizer"))
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var metadata tokenizerDownloadMetadata
|
||||
if err := json.Unmarshal(content, &metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info != "" && !strings.Contains(strings.ToLower(metadata.RepoID), strings.ToLower(info)) {
|
||||
continue
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size, err := files.NewFileOp().GetDirSize(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, dto.LocalModelItem{Name: item.Name, Path: path, RepoID: metadata.RepoID, Source: metadata.Source, Revision: metadata.Revision, DownloadType: "tokenizer", Size: size, SizeFormatted: formatBytes(size), CreatedAt: stat.ModTime().Format("2006-01-02 15:04:05")})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/compose-spec/compose-go/v2/interpolation"
|
||||
"github.com/compose-spec/compose-go/v2/template"
|
||||
@@ -16,76 +18,72 @@ import (
|
||||
|
||||
func LoadModelGPUDevices() dto.ModelGPUDevices {
|
||||
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
|
||||
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern)
|
||||
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern)
|
||||
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern)
|
||||
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
result.Warnings = append(result.Warnings, err.Error())
|
||||
}
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
snapshot, err := client.Collect(context.Background())
|
||||
if err != nil {
|
||||
result.Warnings = append(result.Warnings, err.Error())
|
||||
return result
|
||||
}
|
||||
result.Warnings = append(result.Warnings, snapshot.Info.Warnings...)
|
||||
for _, monitored := range snapshot.Devices {
|
||||
if monitored.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
var device dto.ModelGPUDevice
|
||||
var devicePath string
|
||||
switch directory {
|
||||
case "/proc/driver/nvidia/gpus":
|
||||
informationPath := filepath.Join(directory, entry.Name(), "information")
|
||||
content, err := os.ReadFile(informationPath)
|
||||
device := dto.ModelGPUDevice{Available: true}
|
||||
devicePath := ""
|
||||
switch {
|
||||
case monitored.GPU != nil:
|
||||
if monitored.Vendor == "nvidia" && !re.GetRegex(re.NvidiaGPUUUIDPattern).MatchString(monitored.GPU.UUID) {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID from nvidia-smi: %s", monitored.GPU.UUID))
|
||||
continue
|
||||
}
|
||||
device.ID = monitored.ID
|
||||
device.Name = fmt.Sprintf("GPU %d · %s", monitored.Index, monitored.Name)
|
||||
device.Type = monitored.Vendor
|
||||
device.UUID = monitored.GPU.UUID
|
||||
case monitored.XPU != nil:
|
||||
devicePath = monitored.XPU.Basic.DrmDevice
|
||||
if !re.GetRegex(re.IntelGPUDevicePathPattern).MatchString(devicePath) {
|
||||
directory := filepath.Join("/sys/bus/pci/devices", filepath.Base(monitored.BusID), "drm")
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
result.Warnings = append(result.Warnings, err.Error())
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("resolve Intel GPU %d render device: %v", monitored.Index, err))
|
||||
continue
|
||||
}
|
||||
fields := map[string]string{}
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
key, value, found := strings.Cut(line, ":")
|
||||
if found {
|
||||
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
devicePath = ""
|
||||
for _, entry := range entries {
|
||||
if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
|
||||
devicePath = filepath.Join("/dev/dri", entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
uuid := fields["GPU UUID"]
|
||||
minor, err := strconv.Atoi(fields["Device Minor"])
|
||||
if !uuidPattern.MatchString(uuid) || err != nil || minor < 0 {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID or device minor in %s", informationPath))
|
||||
if devicePath == "" {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
|
||||
continue
|
||||
}
|
||||
name := fields["Model"]
|
||||
if name == "" {
|
||||
name = "NVIDIA"
|
||||
}
|
||||
device = dto.ModelGPUDevice{ID: "nvidia:" + uuid, Name: name + " · " + entry.Name(), Type: "nvidia", UUID: uuid}
|
||||
devicePath = filepath.Join("/dev", "nvidia"+strconv.Itoa(minor))
|
||||
case "/sys/class/drm":
|
||||
if !renderPattern.MatchString(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
vendor, err := os.ReadFile(filepath.Join(directory, entry.Name(), "device/vendor"))
|
||||
if err != nil {
|
||||
result.Warnings = append(result.Warnings, err.Error())
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(vendor)) != "0x8086" {
|
||||
continue
|
||||
}
|
||||
devicePath = filepath.Join("/dev/dri", entry.Name())
|
||||
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"}
|
||||
case "/dev":
|
||||
if !ascendPattern.MatchString(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
devicePath = filepath.Join(directory, entry.Name())
|
||||
device = dto.ModelGPUDevice{ID: "ascend:" + strings.TrimPrefix(entry.Name(), "davinci"), Name: "Ascend · " + entry.Name(), Type: "ascend"}
|
||||
}
|
||||
device.ID = "intel:" + devicePath
|
||||
device.Name = fmt.Sprintf("XPU %d · %s", monitored.Index, monitored.Name)
|
||||
device.Type = "intel"
|
||||
case monitored.NPU != nil:
|
||||
device.ID = fmt.Sprintf("ascend:%d", monitored.Index)
|
||||
device.Name = fmt.Sprintf("NPU %d · Chip %d · %s", monitored.NPUIndex, monitored.ChipIndex, monitored.Name)
|
||||
device.Type = "ascend"
|
||||
devicePath = fmt.Sprintf("/dev/davinci%d", monitored.Index)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if devicePath != "" {
|
||||
info, err := os.Stat(devicePath)
|
||||
device.Available = err == nil && info.Mode()&os.ModeCharDevice != 0
|
||||
if err != nil {
|
||||
result.Warnings = append(result.Warnings, err.Error())
|
||||
} else {
|
||||
device.Available = info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
result.Devices = append(result.Devices, device)
|
||||
}
|
||||
result.Devices = append(result.Devices, device)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -649,9 +647,17 @@ func resolveModelGPUValue(value string, params map[string]interface{}) (string,
|
||||
|
||||
func loadModelAscendDeviceIDs() []string {
|
||||
ids := []string{}
|
||||
for _, device := range LoadModelGPUDevices().Devices {
|
||||
if device.Type == "ascend" && device.Available {
|
||||
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:"))
|
||||
entries, err := os.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return ids
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !re.GetRegex(re.AscendGPUDeviceNamePattern).MatchString(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(filepath.Join("/dev", entry.Name()))
|
||||
if err == nil && info.Mode()&os.ModeCharDevice != 0 {
|
||||
ids = append(ids, strings.TrimPrefix(entry.Name(), "davinci"))
|
||||
}
|
||||
}
|
||||
slices.SortFunc(ids, func(a, b string) int {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/subosito/gotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"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/cmd/server/ai"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
@@ -25,13 +27,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
var tensorRTLLMMu sync.Mutex
|
||||
|
||||
type TensorRTLLMService struct{}
|
||||
|
||||
type ITensorRTLLMService interface {
|
||||
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes
|
||||
Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error)
|
||||
Create(create request.TensorRTLLMCreate) error
|
||||
Update(req request.TensorRTLLMUpdate) error
|
||||
Delete(id uint) error
|
||||
Delete(req request.TensorRTLLMDelete) error
|
||||
Operate(req request.TensorRTLLMOperate) error
|
||||
}
|
||||
|
||||
@@ -39,17 +43,29 @@ func NewITensorRTLLMService() ITensorRTLLMService {
|
||||
return &TensorRTLLMService{}
|
||||
}
|
||||
|
||||
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes {
|
||||
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error) {
|
||||
var (
|
||||
res response.TensorRTLLMsRes
|
||||
items []response.TensorRTLLMDTO
|
||||
)
|
||||
|
||||
total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
|
||||
total, data, err := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
for _, item := range data {
|
||||
_ = syncTensorRTLLMContainerStatus(&item)
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, item.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if operationTask.ID != "" {
|
||||
item.Status = localEngineTaskStatus(operationTask.Operate, item.Status)
|
||||
} else if err := syncTensorRTLLMContainerStatus(&item); err != nil {
|
||||
return res, err
|
||||
}
|
||||
serverDTO := response.TensorRTLLMDTO{
|
||||
TensorRTLLM: item,
|
||||
TaskID: operationTask.ID,
|
||||
}
|
||||
envs, _ := gotenv.Unmarshal(item.Env)
|
||||
serverDTO.Version = envs["VERSION"]
|
||||
@@ -105,7 +121,7 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
|
||||
}
|
||||
res.Total = total
|
||||
res.Items = items
|
||||
return res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) error {
|
||||
@@ -213,6 +229,8 @@ func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gote
|
||||
}
|
||||
|
||||
func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
||||
tensorRTLLMMu.Lock()
|
||||
defer tensorRTLLMMu.Unlock()
|
||||
servers, _ := tensorrtLLMRepo.List()
|
||||
for _, server := range servers {
|
||||
if server.ContainerName == create.ContainerName {
|
||||
@@ -236,11 +254,6 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
||||
if !filesOp.Stat(tensorrtLLMDir) {
|
||||
_ = filesOp.CreateDir(tensorrtLLMDir, 0644)
|
||||
}
|
||||
if create.ModelSpeedup {
|
||||
if err := handleModelArchive(create.ModelType, create.ModelDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tensorrtLLM := &model.TensorRTLLM{
|
||||
Name: create.Name,
|
||||
ContainerName: create.ContainerName,
|
||||
@@ -267,15 +280,23 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
||||
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
|
||||
return err
|
||||
}
|
||||
go startTensorRTLLM(tensorrtLLM)
|
||||
return nil
|
||||
return runTensorRTLLMTask(tensorrtLLM, task.TaskCreate, "create", create.TaskID)
|
||||
}
|
||||
|
||||
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
||||
tensorRTLLMMu.Lock()
|
||||
defer tensorRTLLMMu.Unlock()
|
||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, tensorrtLLM.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if operationTask.ID != "" {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
if tensorrtLLM.ContainerName != req.ContainerName {
|
||||
if err := checkContainerName(req.ContainerName); err != nil {
|
||||
return err
|
||||
@@ -305,59 +326,118 @@ func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
||||
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
|
||||
return err
|
||||
}
|
||||
go startTensorRTLLM(tensorrtLLM)
|
||||
return nil
|
||||
return runTensorRTLLMTask(tensorrtLLM, task.TaskUpdate, "update", req.TaskID)
|
||||
}
|
||||
|
||||
func (t TensorRTLLMService) Delete(id uint) error {
|
||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
||||
_, _ = compose.Down(composePath)
|
||||
_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name))
|
||||
return tensorrtLLMRepo.DeleteBy(repo.WithByID(id))
|
||||
}
|
||||
|
||||
func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
|
||||
func (t TensorRTLLMService) Delete(req request.TensorRTLLMDelete) error {
|
||||
tensorRTLLMMu.Lock()
|
||||
defer tensorRTLLMMu.Unlock()
|
||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
||||
var out string
|
||||
switch req.Operate {
|
||||
case "start":
|
||||
out, err = compose.Up(composePath)
|
||||
tensorrtLLM.Status = constant.StatusRunning
|
||||
case "stop":
|
||||
out, err = compose.Down(composePath)
|
||||
tensorrtLLM.Status = constant.StatusStopped
|
||||
case "restart":
|
||||
out, err = compose.Restart(composePath)
|
||||
tensorrtLLM.Status = constant.StatusRunning
|
||||
}
|
||||
if err != nil {
|
||||
tensorrtLLM.Status = constant.StatusError
|
||||
tensorrtLLM.Message = out
|
||||
}
|
||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
||||
return runTensorRTLLMTask(tensorrtLLM, task.TaskDelete, "delete", req.TaskID)
|
||||
}
|
||||
|
||||
func startTensorRTLLM(tensorrtLLM *model.TensorRTLLM) {
|
||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
||||
if tensorrtLLM.Status != constant.StatusNormal {
|
||||
_, _ = compose.Down(composePath)
|
||||
func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
|
||||
tensorRTLLMMu.Lock()
|
||||
defer tensorRTLLMMu.Unlock()
|
||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := compose.Up(composePath); err != nil {
|
||||
tensorrtLLM.Status = constant.StatusError
|
||||
tensorrtLLM.Message = out
|
||||
} else {
|
||||
tensorrtLLM.Status = constant.StatusRunning
|
||||
tensorrtLLM.Message = ""
|
||||
var operation string
|
||||
switch req.Operate {
|
||||
case "start":
|
||||
operation = "Run"
|
||||
case "stop":
|
||||
operation = "Stop"
|
||||
case "restart":
|
||||
operation = task.TaskRestart
|
||||
default:
|
||||
return fmt.Errorf("unsupported TensorRT operation: %s", req.Operate)
|
||||
}
|
||||
_ = syncTensorRTLLMContainerStatus(tensorrtLLM)
|
||||
return runTensorRTLLMTask(tensorrtLLM, operation, req.Operate, req.TaskID)
|
||||
}
|
||||
|
||||
func runTensorRTLLMTask(llm *model.TensorRTLLM, operation, action, taskID string) error {
|
||||
currentTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, llm.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if currentTask.ID != "" {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
operationTask, err := task.NewTaskWithOps(llm.Name, operation, task.TaskScopeTensorRT, taskID, llm.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
operationTask.AddSubTaskWithOps(operationTask.Name, func(t *task.Task) error {
|
||||
if action == "stop" {
|
||||
llm.Status = constant.StatusWaiting
|
||||
} else {
|
||||
llm.Status = localEngineTaskStatus(operation, constant.StatusStarting)
|
||||
}
|
||||
if err := tensorrtLLMRepo.Save(llm); err != nil {
|
||||
return err
|
||||
}
|
||||
composePath := path.Join(global.Dir.TensorRTLLMDir, llm.Name, "docker-compose.yml")
|
||||
var out string
|
||||
var err error
|
||||
switch action {
|
||||
case "create":
|
||||
if llm.ModelSpeedup {
|
||||
envs, err := gotenv.Unmarshal(llm.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := handleModelArchive(llm.ModelType, envs["MODEL_PATH"]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
out, err = compose.Up(composePath)
|
||||
case "start":
|
||||
out, err = compose.Up(composePath)
|
||||
case "update":
|
||||
out, err = compose.Down(composePath)
|
||||
if strings.TrimSpace(out) != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err = compose.Up(composePath)
|
||||
case "stop":
|
||||
out, err = compose.Stop(composePath)
|
||||
case "restart":
|
||||
out, err = compose.Restart(composePath)
|
||||
case "delete":
|
||||
out, err = compose.Down(composePath)
|
||||
default:
|
||||
return fmt.Errorf("unsupported TensorRT operation: %s", action)
|
||||
}
|
||||
if strings.TrimSpace(out) != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if action == "delete" {
|
||||
if err := files.NewFileOp().DeleteDir(path.Join(global.Dir.TensorRTLLMDir, llm.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
return tensorrtLLMRepo.DeleteBy(repo.WithByID(llm.ID))
|
||||
}
|
||||
if err := syncTensorRTLLMContainerStatus(llm); err != nil {
|
||||
return err
|
||||
}
|
||||
llm.Message = ""
|
||||
return tensorrtLLMRepo.Save(llm)
|
||||
}, nil, 0, 0)
|
||||
go func() {
|
||||
_ = operationTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
|
||||
@@ -372,28 +452,24 @@ func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
|
||||
return err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
if tensorrtLLM.Status == constant.StatusStarting {
|
||||
return nil
|
||||
}
|
||||
tensorrtLLM.Status = constant.StatusStopped
|
||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
||||
tensorrtLLM.Status = constant.StatusError
|
||||
tensorrtLLM.Message = buserr.WithName("ErrContainerNotFound", tensorrtLLM.ContainerName).Error()
|
||||
return nil
|
||||
}
|
||||
container := containers[0]
|
||||
switch container.State {
|
||||
case "exited":
|
||||
tensorrtLLM.Status = constant.StatusError
|
||||
tensorrtLLM.Status = constant.StatusStopped
|
||||
case "running":
|
||||
tensorrtLLM.Status = constant.StatusRunning
|
||||
case "paused":
|
||||
tensorrtLLM.Status = constant.StatusStopped
|
||||
tensorrtLLM.Status = constant.StatusPaused
|
||||
case "restarting":
|
||||
tensorrtLLM.Status = constant.StatusRestarting
|
||||
default:
|
||||
if tensorrtLLM.Status != constant.StatusStarting {
|
||||
tensorrtLLM.Status = constant.StatusStopped
|
||||
}
|
||||
tensorrtLLM.Status = constant.StatusUnHealthy
|
||||
}
|
||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
||||
return nil
|
||||
}
|
||||
|
||||
func findModelArchive(modelType string) (string, error) {
|
||||
|
||||
@@ -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, `/\\`)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -280,6 +280,8 @@ func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp {
|
||||
}
|
||||
|
||||
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
|
||||
websiteListenMutation.Lock()
|
||||
defer websiteListenMutation.Unlock()
|
||||
alias := create.Alias
|
||||
if alias == "default" {
|
||||
return buserr.New("ErrDefaultAlias")
|
||||
@@ -377,6 +379,11 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
website.HttpConfig = sslReq.HttpConfig
|
||||
initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq}
|
||||
}
|
||||
candidate := *website
|
||||
candidate.Domains = domains
|
||||
if err = checkWebsiteListenConflict(candidate, defaultHttpPort, nginxInstall.HttpsPort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0)
|
||||
@@ -1004,6 +1011,8 @@ func applyWebsiteHTTPSParams(res *response.WebsiteHTTPS, params []response.Nginx
|
||||
}
|
||||
|
||||
func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) {
|
||||
websiteListenMutation.Lock()
|
||||
defer websiteListenMutation.Unlock()
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1012,6 +1021,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH
|
||||
res response.WebsiteHTTPS
|
||||
websiteSSL model.WebsiteSSL
|
||||
)
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidate := website
|
||||
candidate.Protocol, candidate.HttpConfig = constant.ProtocolHTTP, req.HttpConfig
|
||||
if req.Enable {
|
||||
candidate.Protocol = constant.ProtocolHTTPS
|
||||
}
|
||||
if err = checkWebsiteListenConflict(candidate, nginxInstall.HttpPort, nginxInstall.HttpsPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = ChangeHSTSConfig(req.Hsts, req.HstsIncludeSubDomains, req.Http3, website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
|
||||
websiteListenMutation.Lock()
|
||||
defer websiteListenMutation.Unlock()
|
||||
var (
|
||||
domainModels []model.WebsiteDomain
|
||||
addPorts []int
|
||||
@@ -31,6 +33,15 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports := websiteListenPorts(website, httpPort, httpsPort)
|
||||
candidate := website
|
||||
candidate.Domains = append(append([]model.WebsiteDomain(nil), website.Domains...), domainModels...)
|
||||
for _, domain := range domainModels {
|
||||
ports[domain.Port] = struct{}{}
|
||||
}
|
||||
if err = checkWebsiteListenPorts(candidate, ports, httpPort, httpsPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
if err := ensureFirewallPorts(addPorts); err != nil {
|
||||
global.LOG.Errorf("allow website firewall ports failed: %v", err)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
var websiteListenMutation sync.Mutex
|
||||
|
||||
func websiteListenPorts(website model.Website, httpPort, httpsPort int) map[int]struct{} {
|
||||
ports := make(map[int]struct{})
|
||||
for _, domain := range website.Domains {
|
||||
if domain.Port > 0 {
|
||||
ports[domain.Port] = struct{}{}
|
||||
}
|
||||
}
|
||||
if website.Protocol == constant.ProtocolHTTPS {
|
||||
plan := buildWebsiteTLSPlan(website.Domains, httpPort, httpsPort)
|
||||
for _, port := range plan.httpsPorts {
|
||||
if port > 0 {
|
||||
ports[port] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func checkWebsiteListenConflict(website model.Website, httpPort, httpsPort int) error {
|
||||
return checkWebsiteListenPorts(website, websiteListenPorts(website, httpPort, httpsPort), httpPort, httpsPort)
|
||||
}
|
||||
|
||||
func checkWebsiteListenPorts(website model.Website, ports map[int]struct{}, httpPort, httpsPort int) error {
|
||||
if len(website.Domains) == 0 || len(ports) == 0 {
|
||||
return nil
|
||||
}
|
||||
names := make(map[string]struct{}, len(website.Domains))
|
||||
for _, domain := range website.Domains {
|
||||
names[strings.ToLower(domain.Domain)] = struct{}{}
|
||||
}
|
||||
existing, err := websiteRepo.GetBy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domains, err := websiteDomainRepo.GetBy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byWebsite := make(map[uint][]model.WebsiteDomain)
|
||||
for _, domain := range domains {
|
||||
byWebsite[domain.WebsiteID] = append(byWebsite[domain.WebsiteID], domain)
|
||||
}
|
||||
for _, other := range existing {
|
||||
if website.ID != 0 && other.ID == website.ID || other.Type == constant.Stream {
|
||||
continue
|
||||
}
|
||||
other.Domains = byWebsite[other.ID]
|
||||
for _, domain := range other.Domains {
|
||||
if _, match := names[strings.ToLower(domain.Domain)]; !match {
|
||||
continue
|
||||
}
|
||||
for port := range websiteListenPorts(other, httpPort, httpsPort) {
|
||||
if _, occupied := ports[port]; occupied {
|
||||
return buserr.WithName("ErrDomainIsUsed", other.PrimaryDomain)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -81,6 +81,7 @@ const (
|
||||
const (
|
||||
TaskScopeWebsite = "Website"
|
||||
TaskScopeAI = "AI"
|
||||
TaskScopeTensorRT = "TensorRT"
|
||||
TaskScopeApp = "App"
|
||||
TaskScopeRuntime = "Runtime"
|
||||
TaskScopeDatabase = "Database"
|
||||
|
||||
+130
-121
@@ -8,6 +8,107 @@
|
||||
"formatZH": "更新 Ollama 绑定域名 [domain]",
|
||||
"formatEN": "update Ollama bind domain [domain]"
|
||||
},
|
||||
"/ai/model/downloader/featured/sync/status": {
|
||||
"bodyKeys": [
|
||||
"value"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新推荐模型自动同步 [value]",
|
||||
"formatEN": "update featured model automatic synchronization [value]"
|
||||
},
|
||||
"/ai/model/downloader/hf/download": {
|
||||
"bodyKeys": [
|
||||
"repoID",
|
||||
"revision"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 Hugging Face 模型 [repoID]",
|
||||
"formatEN": "download Hugging Face model [repoID]"
|
||||
},
|
||||
"/ai/model/downloader/local/delete": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除本地模型 [name]",
|
||||
"formatEN": "delete local model [name]"
|
||||
},
|
||||
"/ai/model/downloader/modelscope/download": {
|
||||
"bodyKeys": [
|
||||
"repoID",
|
||||
"revision"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 ModelScope 模型 [repoID]",
|
||||
"formatEN": "download ModelScope model [repoID]"
|
||||
},
|
||||
"/ai/model/downloader/settings": {
|
||||
"bodyKeys": [
|
||||
"modelDir"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新模型下载设置 [modelDir]",
|
||||
"formatEN": "update model downloader settings [modelDir]"
|
||||
},
|
||||
"/ai/model/downloader/tasks/cancel": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "取消模型下载任务 [repoID]",
|
||||
"formatEN": "cancel model download task [repoID]"
|
||||
},
|
||||
"/ai/model/downloader/tasks/remove": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "移除模型下载任务 [repoID]",
|
||||
"formatEN": "remove model download task [repoID]"
|
||||
},
|
||||
"/ai/model/downloader/tasks/retry": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "重试模型下载任务 [repoID]",
|
||||
"formatEN": "retry model download task [repoID]"
|
||||
},
|
||||
"/ai/ollama/close": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
@@ -103,27 +204,27 @@
|
||||
"/alert/config/del": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除告警配置 [name]",
|
||||
"formatEN": "delete alert config [name]",
|
||||
"beforeFunctions": []
|
||||
"formatEN": "delete alert config [name]"
|
||||
},
|
||||
"/alert/config/status": {
|
||||
"bodyKeys": [
|
||||
"status"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新告警配置状态 [name][status]",
|
||||
"formatEN": "update alert config status [name][status]",
|
||||
"beforeFunctions": []
|
||||
"formatEN": "update alert config status [name][status]"
|
||||
},
|
||||
"/alert/config/update": {
|
||||
"bodyKeys": [
|
||||
"displayName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新告警配置 [displayName]",
|
||||
"formatEN": "update alert config [displayName]",
|
||||
"beforeFunctions": []
|
||||
"formatEN": "update alert config [displayName]"
|
||||
},
|
||||
"/alert/del": {
|
||||
"bodyKeys": [
|
||||
@@ -4971,98 +5072,6 @@
|
||||
"formatZH": "更新离线告警日志 [id]",
|
||||
"formatEN": "update offline alert log [id]"
|
||||
},
|
||||
"/xpack/model/downloader/hf/download": {
|
||||
"bodyKeys": [
|
||||
"repoID",
|
||||
"revision"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 Hugging Face 模型 [repoID]",
|
||||
"formatEN": "download Hugging Face model [repoID]"
|
||||
},
|
||||
"/xpack/model/downloader/local/delete": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除本地模型 [name]",
|
||||
"formatEN": "delete local model [name]"
|
||||
},
|
||||
"/xpack/model/downloader/modelscope/download": {
|
||||
"bodyKeys": [
|
||||
"repoID",
|
||||
"revision"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 ModelScope 模型 [repoID]",
|
||||
"formatEN": "download ModelScope model [repoID]"
|
||||
},
|
||||
"/xpack/model/downloader/settings": {
|
||||
"bodyKeys": [
|
||||
"modelDir"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新模型下载设置 [modelDir]",
|
||||
"formatEN": "update model downloader settings [modelDir]"
|
||||
},
|
||||
"/xpack/model/downloader/tasks/cancel": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "取消模型下载任务 [repoID]",
|
||||
"formatEN": "cancel model download task [repoID]"
|
||||
},
|
||||
"/xpack/model/downloader/tasks/remove": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "移除模型下载任务 [repoID]",
|
||||
"formatEN": "remove model download task [repoID]"
|
||||
},
|
||||
"/xpack/model/downloader/tasks/retry": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "model_download_tasks",
|
||||
"output_column": "repo_id",
|
||||
"output_value": "repoID"
|
||||
}
|
||||
],
|
||||
"formatZH": "重试模型下载任务 [repoID]",
|
||||
"formatEN": "retry model download task [repoID]"
|
||||
},
|
||||
"/xpack/monitor/config/global": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
@@ -5211,7 +5220,7 @@
|
||||
"formatZH": "更新 vLLM [name]",
|
||||
"formatEN": "update vLLM [name]"
|
||||
},
|
||||
"/xpack/vms": {
|
||||
"/vms": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
@@ -5220,7 +5229,7 @@
|
||||
"formatZH": "创建虚拟机 [name]",
|
||||
"formatEN": "create VM [name]"
|
||||
},
|
||||
"/xpack/vms/del": {
|
||||
"/vms/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5238,7 +5247,7 @@
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
"/xpack/vms/disks/resize": {
|
||||
"/vms/disks/resize": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"target",
|
||||
@@ -5249,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"
|
||||
@@ -5266,7 +5275,7 @@
|
||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||
"formatEN": "create VM ISO [name] [type]"
|
||||
},
|
||||
"/xpack/vms/iso/del": {
|
||||
"/vms/iso/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5284,7 +5293,7 @@
|
||||
"formatZH": "删除虚拟机镜像 [name]",
|
||||
"formatEN": "delete VM ISO [name]"
|
||||
},
|
||||
"/xpack/vms/iso/update": {
|
||||
"/vms/iso/update": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -5294,7 +5303,7 @@
|
||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||
"formatEN": "update VM ISO [name] [type]"
|
||||
},
|
||||
"/xpack/vms/networks": {
|
||||
"/vms/networks": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -5304,7 +5313,7 @@
|
||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||
"formatEN": "create VM network [name] [type]"
|
||||
},
|
||||
"/xpack/vms/networks/del": {
|
||||
"/vms/networks/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5322,7 +5331,7 @@
|
||||
"formatZH": "删除虚拟机网络 [name]",
|
||||
"formatEN": "delete VM network [name]"
|
||||
},
|
||||
"/xpack/vms/networks/update": {
|
||||
"/vms/networks/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5340,7 +5349,7 @@
|
||||
"formatZH": "更新虚拟机网络 [name]",
|
||||
"formatEN": "update VM network [name]"
|
||||
},
|
||||
"/xpack/vms/operate": {
|
||||
"/vms/operate": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"operate"
|
||||
@@ -5350,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"
|
||||
@@ -5367,7 +5376,7 @@
|
||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||
"formatEN": "rename VM [name] to [newName]"
|
||||
},
|
||||
"/xpack/vms/snapshots": {
|
||||
"/vms/snapshots": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -5377,7 +5386,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/snapshots/del": {
|
||||
"/vms/snapshots/del": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -5387,7 +5396,7 @@
|
||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/snapshots/recover": {
|
||||
"/vms/snapshots/recover": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -5397,7 +5406,7 @@
|
||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/storages": {
|
||||
"/vms/storages": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -5407,7 +5416,7 @@
|
||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||
"formatEN": "create VM storage [name] [type]"
|
||||
},
|
||||
"/xpack/vms/storages/del": {
|
||||
"/vms/storages/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5425,7 +5434,7 @@
|
||||
"formatZH": "删除虚拟机存储 [name]",
|
||||
"formatEN": "delete VM storage [name]"
|
||||
},
|
||||
"/xpack/vms/storages/update": {
|
||||
"/vms/storages/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5443,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"
|
||||
@@ -5460,7 +5469,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||
"formatEN": "create VM [name] template [templateName]"
|
||||
},
|
||||
"/xpack/vms/templates/del": {
|
||||
"/vms/templates/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -5478,7 +5487,7 @@
|
||||
"formatZH": "删除虚拟机模板 [name]",
|
||||
"formatEN": "delete VM template [name]"
|
||||
},
|
||||
"/xpack/vms/update": {
|
||||
"/vms/update": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
|
||||
+11
-1
@@ -26,6 +26,12 @@ func Run() {
|
||||
interval model.Setting
|
||||
status model.Setting
|
||||
)
|
||||
if !global.CONF.Base.IsOffline {
|
||||
if _, err := global.Cron.AddFunc("@every 24h", service.SyncFeaturedModels); err != nil {
|
||||
global.LOG.Errorf("add featured model sync job failed: %v", err)
|
||||
}
|
||||
go service.SyncFeaturedModels()
|
||||
}
|
||||
go syncBeforeStart()
|
||||
if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil {
|
||||
global.LOG.Errorf("load monitor status from db failed, err: %v", err)
|
||||
@@ -77,7 +83,11 @@ func Run() {
|
||||
global.LOG.Errorf("generate random hour failed: %v", err)
|
||||
hourRand = big.NewInt(0)
|
||||
}
|
||||
if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), job.NewAppStoreJob()); err != nil {
|
||||
appSyncSchedule := fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64())
|
||||
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewContainerImageCheckUpdateJob()); err != nil {
|
||||
global.LOG.Errorf("cannot add container image update cron job: %v", err)
|
||||
}
|
||||
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewAppStoreJob()); err != nil {
|
||||
global.LOG.Errorf("can not add appstore corn job: %s", err.Error())
|
||||
}
|
||||
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
type containerImageCheckUpdate struct{}
|
||||
|
||||
func NewContainerImageCheckUpdateJob() *containerImageCheckUpdate {
|
||||
return &containerImageCheckUpdate{}
|
||||
}
|
||||
func (j *containerImageCheckUpdate) Run() {
|
||||
status, err := service.GetContainerImageCheckUpdateStatus()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load container image update schedule: %v", err)
|
||||
return
|
||||
}
|
||||
if status.Enabled {
|
||||
if _, err := service.StartContainerImageCheckUpdate(); err != nil {
|
||||
global.LOG.Errorf("start container image update check: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ module github.com/1Panel-dev/1Panel/agent
|
||||
|
||||
go 1.27.3
|
||||
|
||||
godebug multipathtcp=0
|
||||
|
||||
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
|
||||
|
||||
require (
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Requested resource not found: {{ .err }}'
|
||||
|
||||
# quick jump
|
||||
ErrMinQuickJump: 'Set at least one quick jump entry'
|
||||
ErrMaxQuickJump: 'At most 4 quick jump entries'
|
||||
ErrMaxQuickJump: 'At most 5 quick jump entries'
|
||||
|
||||
# common
|
||||
ErrShutDown: 'Command stopped manually'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Unsupported agent provider'
|
||||
ErrAgentAccountRequired: 'Select an agent account first'
|
||||
ErrAgentAccountNotVerified: 'Agent account not verified'
|
||||
ErrAgentAccountModelsRequired: 'Please configure at least one model'
|
||||
ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation'
|
||||
ErrAgentAccountSingleInitialModel: 'This provider supports configuring at most one model'
|
||||
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
|
||||
ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it'
|
||||
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'The certificate file does not exist'
|
||||
ErrSSLKeyFormat: 'Private key file verification failed'
|
||||
ErrSSLCertificateFormat: 'Invalid certificate format; use PEM'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank'
|
||||
ErrOpenrestyInstall: "Please install OpenResty from the App Store first"
|
||||
ErrOpenrestyNotFound: 'HTTP mode requires OpenResty'
|
||||
ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]'
|
||||
dnsAccount: 'DNS Auto'
|
||||
@@ -735,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"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use
|
||||
AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "Check for image updates"
|
||||
ContainerImageCheckScope: "Checking all {{ .count }} containers on this node, regardless of pagination or filters"
|
||||
ContainerImageCheckItem: "Checking container {{ .name }}, image {{ .image }}"
|
||||
ContainerImageCheckFailed: "Container {{ .name }} check failed: {{ .error }}; retaining previous results matching the current image"
|
||||
ContainerImageCheckSkipped: "Skipping {{ .name }}: not a tagged Docker Hub image, a local image, or pinned digest"
|
||||
ContainerImageCheckAvailable: "Container {{ .name }} has an update: {{ .image }} -> {{ .target }}"
|
||||
ContainerImageCheckCurrent: "No update found for container {{ .name }}"
|
||||
ContainerImageCheckChanged: "Container {{ .name }} was removed or its image changed; result was not saved"
|
||||
ContainerImageCheckSummary: "Check finished: {{ .checked }} checked, {{ .updates }} upgradable, {{ .failed }} failed, {{ .skipped }} skipped"
|
||||
ContainerImageCheckFailures: "{{ .count }} container checks failed; see the task log"
|
||||
|
||||
ErrImageCheckProxyRequired: "To check for image updates in the current runtime environment, please configure a proxy server first."
|
||||
|
||||
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'No se pudo encontrar el recurso solicitado {{ .err }}'
|
||||
|
||||
# salto rápido
|
||||
ErrMinQuickJump: 'Agrega al menos una entrada rápida'
|
||||
ErrMaxQuickJump: 'Máximo 4 entradas rápidas'
|
||||
ErrMaxQuickJump: 'Máximo 5 entradas rápidas'
|
||||
|
||||
# común
|
||||
ErrShutDown: 'Comando detenido manualmente'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
|
||||
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
|
||||
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
|
||||
ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
|
||||
ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta'
|
||||
ErrAgentAccountSingleInitialModel: 'Este proveedor permite configurar como máximo un modelo'
|
||||
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
|
||||
ErrAgentVerifyModelInUse: 'Este modelo se usa para verificar la cuenta. Seleccione otro modelo de verificación antes de eliminarlo'
|
||||
ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'El archivo de certificado no existe'
|
||||
ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada'
|
||||
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
|
||||
ErrOpenrestyInstall: "Instale primero OpenResty desde la tienda de aplicaciones"
|
||||
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
|
||||
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
|
||||
dnsAccount: 'DNS Automático'
|
||||
@@ -735,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"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración co
|
||||
AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "Buscar actualizaciones de imágenes"
|
||||
ContainerImageCheckScope: "Comprobando los {{ .count }} contenedores de este nodo, sin aplicar paginación ni filtros"
|
||||
ContainerImageCheckItem: "Comprobando el contenedor {{ .name }}, imagen {{ .image }}"
|
||||
ContainerImageCheckFailed: "Error al comprobar el contenedor {{ .name }}: {{ .error }}; se conservan los resultados anteriores correspondientes a la imagen actual"
|
||||
ContainerImageCheckSkipped: "Se omite {{ .name }}: no es una imagen de Docker Hub con etiqueta, es una imagen local o tiene un digest fijo"
|
||||
ContainerImageCheckAvailable: "El contenedor {{ .name }} tiene una actualización: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "No se encontraron actualizaciones para el contenedor {{ .name }}"
|
||||
ContainerImageCheckChanged: "El contenedor {{ .name }} se eliminó o su imagen cambió; no se guardó el resultado"
|
||||
ContainerImageCheckSummary: "Comprobación finalizada: comprobados {{ .checked }}, actualizables {{ .updates }}, fallidos {{ .failed }}, omitidos {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "Falló la comprobación de {{ .count }} contenedores; consulte el registro de la tarea"
|
||||
|
||||
ErrImageCheckProxyRequired: "Para comprobar actualizaciones de imágenes en el entorno actual, configura primero un servidor proxy."
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'منبع درخواستی یافت نشد: {{ .err }}'
|
||||
|
||||
# پرش سریع
|
||||
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
|
||||
ErrMaxQuickJump: 'حداکثر ۴ ورودی پرش سریع'
|
||||
ErrMaxQuickJump: 'حداکثر ۵ ورودی پرش سریع'
|
||||
|
||||
# عمومی
|
||||
ErrShutDown: 'دستور به صورت دستی متوقف شد'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'ارائهدهنده عامل پشتیبانی
|
||||
ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید'
|
||||
ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است'
|
||||
ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید'
|
||||
ErrAgentAccountSingleInitialModel: 'فقط یک مدل اولیه میتواند در هنگام ایجاد حساب پیکربندی شود'
|
||||
ErrAgentAccountSingleInitialModel: 'برای این ارائهدهنده حداکثر یک مدل میتوان پیکربندی کرد'
|
||||
ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است'
|
||||
ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده میشود. پیش از حذف، مدل بررسی دیگری انتخاب کنید'
|
||||
ErrAgentFallbackModelDuplicate: 'مدلهای جایگزین نمیتوانند تکراری باشند'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'فایل گواهی وجود ندارد'
|
||||
ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود'
|
||||
ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمیتوانند خالی باشند'
|
||||
ErrOpenrestyInstall: "لطفاً ابتدا OpenResty را از فروشگاه برنامهها نصب کنید"
|
||||
ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد'
|
||||
ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]'
|
||||
dnsAccount: 'خودکار DNS'
|
||||
@@ -736,7 +737,7 @@ ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندار
|
||||
# ماشین مجازی
|
||||
ErrVMNotFound: "ماشین مجازی وجود ندارد"
|
||||
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
|
||||
ErrVMCountLimit: "در نسخه Professional میتوانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
|
||||
ErrVMCountLimit: "در نسخه فعلی میتوانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه بالاتر ارتقا دهید و دوباره تلاش کنید."
|
||||
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "مسیر استخر ذخیرهسازی قبلاً توسط [{{ .name }}] استفاده شده است"
|
||||
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
|
||||
@@ -803,3 +804,15 @@ ErrFeishuUpgradeMounts: "این اتصالهای ذخیرهسازی کام
|
||||
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "بررسی بهروزرسانی ایمیجها"
|
||||
ContainerImageCheckScope: "در حال بررسی همه {{ .count }} کانتینر این گره، بدون توجه به صفحهبندی و فیلترها"
|
||||
ContainerImageCheckItem: "در حال بررسی کانتینر {{ .name }}، ایمیج {{ .image }}"
|
||||
ContainerImageCheckFailed: "بررسی کانتینر {{ .name }} ناموفق بود: {{ .error }}؛ نتایج قبلیِ منطبق با ایمیج فعلی حفظ میشوند"
|
||||
ContainerImageCheckSkipped: "کانتینر {{ .name }} رد شد: ایمیج تگدار Docker Hub نیست، ایمیج محلی است یا digest ثابت دارد"
|
||||
ContainerImageCheckAvailable: "برای کانتینر {{ .name }} بهروزرسانی موجود است: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "بهروزرسانی برای کانتینر {{ .name }} یافت نشد"
|
||||
ContainerImageCheckChanged: "کانتینر {{ .name }} حذف شده یا ایمیج آن تغییر کرده است؛ نتیجه ذخیره نشد"
|
||||
ContainerImageCheckSummary: "بررسی پایان یافت: بررسیشده {{ .checked }}، قابل ارتقا {{ .updates }}، ناموفق {{ .failed }}، ردشده {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "بررسی {{ .count }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید"
|
||||
|
||||
ErrImageCheckProxyRequired: "برای بررسی بهروزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید."
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '要求されたリソース {{ .err }} が見つかりま
|
||||
|
||||
# クイックジャンプ
|
||||
ErrMinQuickJump: 'クイックアクセスを1件以上追加'
|
||||
ErrMaxQuickJump: '最大4件のクイックアクセス'
|
||||
ErrMaxQuickJump: '最大5件のクイックアクセス'
|
||||
|
||||
# 共通
|
||||
ErrShutDown: 'コマンドは手動で停止されました'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
||||
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
||||
ErrAgentAccountNotVerified: 'アカウント未検証です'
|
||||
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
|
||||
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
|
||||
ErrAgentAccountSingleInitialModel: 'このプロバイダーで設定できるモデルは最大 1 つです'
|
||||
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
||||
ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください'
|
||||
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '証明書ファイルが存在しません'
|
||||
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
|
||||
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
|
||||
ErrOpenrestyInstall: "先にアプリストアから OpenResty をインストールしてください"
|
||||
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
|
||||
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
|
||||
dnsAccount: 'DNS 自動'
|
||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
|
||||
# 仮想マシン
|
||||
ErrVMNotFound: "仮想マシンが存在しません"
|
||||
ErrVMAlreadyExists: "仮想マシンは既に存在します"
|
||||
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
|
||||
ErrVMCountLimit: "現在のエディションでは最大 {{ .limit }} 台の仮想マシンを作成できます。 既存の仮想マシンを削除するか、上位エディションにアップグレードしてから再試行してください。"
|
||||
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
|
||||
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・
|
||||
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "イメージの更新を確認"
|
||||
ContainerImageCheckScope: "ページ分割やフィルターに関係なく、このノードの全 {{ .count }} コンテナを確認します"
|
||||
ContainerImageCheckItem: "コンテナ {{ .name }}、イメージ {{ .image }} を確認しています"
|
||||
ContainerImageCheckFailed: "コンテナ {{ .name }} の確認に失敗しました:{{ .error }}。現在のイメージに対応する前回の結果を保持します"
|
||||
ContainerImageCheckSkipped: "{{ .name }} をスキップ:タグ付きの Docker Hub イメージではないか、ローカルイメージまたは digest 固定のイメージです"
|
||||
ContainerImageCheckAvailable: "コンテナ {{ .name }} は更新可能です:{{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "コンテナ {{ .name }} の更新は見つかりませんでした"
|
||||
ContainerImageCheckChanged: "コンテナ {{ .name }} が削除されたか、イメージが変更されたため、結果は保存されませんでした"
|
||||
ContainerImageCheckSummary: "確認終了:成功 {{ .checked }}、更新可能 {{ .updates }}、失敗 {{ .failed }}、スキップ {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "{{ .count }} コンテナの確認に失敗しました。タスクログを確認してください"
|
||||
|
||||
ErrImageCheckProxyRequired: "現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。"
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '요청한 리소스 {{ .err }}을 찾을 수 없습니다.'
|
||||
|
||||
# 빠른 이동
|
||||
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
|
||||
ErrMaxQuickJump: '퀵 액세스는 최대 4개'
|
||||
ErrMaxQuickJump: '퀵 액세스는 최대 5개'
|
||||
|
||||
# 공통
|
||||
ErrShutDown: '명령이 수동으로 중단되었습니다'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
||||
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
||||
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
||||
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
|
||||
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
|
||||
ErrAgentAccountSingleInitialModel: '이 제공업체에는 모델을 최대 하나만 설정할 수 있습니다'
|
||||
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
||||
ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요'
|
||||
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '인증서 파일이 존재하지 않습니다'
|
||||
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
|
||||
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
|
||||
ErrOpenrestyInstall: "먼저 앱 스토어에서 OpenResty를 설치하세요"
|
||||
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
|
||||
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
|
||||
dnsAccount: 'DNS 자동'
|
||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
|
||||
# 가상 머신
|
||||
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
|
||||
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
|
||||
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
|
||||
ErrVMCountLimit: "현재 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 상위 버전으로 업그레이드한 후 다시 시도하세요."
|
||||
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
|
||||
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원
|
||||
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "이미지 업데이트 확인"
|
||||
ContainerImageCheckScope: "페이지 및 필터와 관계없이 현재 노드의 모든 컨테이너 {{ .count }}개를 확인합니다"
|
||||
ContainerImageCheckItem: "컨테이너 {{ .name }}, 이미지 {{ .image }} 확인 중"
|
||||
ContainerImageCheckFailed: "컨테이너 {{ .name }} 확인 실패: {{ .error }}. 현재 이미지와 일치하는 이전 결과를 유지합니다"
|
||||
ContainerImageCheckSkipped: "{{ .name }} 건너뜀: 태그가 있는 Docker Hub 이미지가 아니거나 로컬 이미지 또는 digest가 고정된 이미지입니다"
|
||||
ContainerImageCheckAvailable: "컨테이너 {{ .name }} 업데이트 가능: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "컨테이너 {{ .name }}의 업데이트를 찾지 못했습니다"
|
||||
ContainerImageCheckChanged: "컨테이너 {{ .name }}이 삭제되었거나 이미지가 변경되어 결과를 저장하지 않았습니다"
|
||||
ContainerImageCheckSummary: "확인 종료: 성공 {{ .checked }}, 업데이트 가능 {{ .updates }}, 실패 {{ .failed }}, 건너뜀 {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "컨테이너 {{ .count }}개 확인에 실패했습니다. 작업 로그를 확인하세요"
|
||||
|
||||
ErrImageCheckProxyRequired: "현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요."
|
||||
|
||||
+16
-3
@@ -32,7 +32,7 @@ ErrApiConfigKeyTimeInvalid: 'ເວລາ (Timestamp) ຂອງ API ບໍ່ຖ
|
||||
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
|
||||
|
||||
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
|
||||
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 4 ລາຍການ'
|
||||
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 5 ລາຍການ'
|
||||
|
||||
#common
|
||||
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
|
||||
@@ -62,7 +62,7 @@ ErrAgentProviderNotSupported: 'ບໍ່ຮອງຮັບຜູ້ໃຫ້ບ
|
||||
ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ'
|
||||
ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ'
|
||||
ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ'
|
||||
ErrAgentAccountSingleInitialModel: 'ສາມາດກຳນົດຄ່າໂມເດລເລີ່ມຕົ້ນໄດ້ພຽງຕົວດຽວໃນລະຫວ່າງການສ້າງບັນຊີ'
|
||||
ErrAgentAccountSingleInitialModel: 'ຜູ້ໃຫ້ບໍລິການນີ້ສາມາດກຳນົດຄ່າໄດ້ສູງສຸດໜຶ່ງໂມເດວ'
|
||||
ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ'
|
||||
ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.'
|
||||
ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້'
|
||||
@@ -242,6 +242,7 @@ ErrSSLCertificateNotFound: 'ບໍ່ມີໄຟລ໌ໃບຮັບຮອງ
|
||||
ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ'
|
||||
ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້'
|
||||
ErrOpenrestyInstall: "ກະລຸນາຕິດຕັ້ງ OpenResty ຈາກຮ້ານແອັບກ່ອນ"
|
||||
ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty'
|
||||
ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]'
|
||||
dnsAccount: 'DNS ອັດຕະໂນມັດ'
|
||||
@@ -726,7 +727,7 @@ ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນ
|
||||
# virtual machine
|
||||
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
|
||||
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
|
||||
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
|
||||
ErrVMCountLimit: "ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດລຸ້ນ ແລ້ວລອງອີກຄັ້ງ."
|
||||
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
|
||||
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
|
||||
@@ -793,3 +794,15 @@ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້
|
||||
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "ກວດສອບການອັບເດດອິມເມຈ"
|
||||
ContainerImageCheckScope: "ກຳລັງກວດສອບທັງໝົດ {{ .count }} ຄອນເທນເນີໃນໂນດນີ້ ໂດຍບໍ່ຂຶ້ນກັບການແບ່ງໜ້າ ຫຼື ຕົວກອງ"
|
||||
ContainerImageCheckItem: "ກຳລັງກວດສອບຄອນເທນເນີ {{ .name }}, ອິມເມຈ {{ .image }}"
|
||||
ContainerImageCheckFailed: "ກວດສອບຄອນເທນເນີ {{ .name }} ລົ້ມເຫຼວ: {{ .error }}; ເກັບຜົນກ່ອນໜ້າທີ່ກົງກັບອິມເມຈປັດຈຸບັນໄວ້"
|
||||
ContainerImageCheckSkipped: "ຂ້າມ {{ .name }}: ບໍ່ແມ່ນອິມເມຈ Docker Hub ທີ່ມີແທັກ, ເປັນອິມເມຈພາຍໃນ ຫຼື ກຳນົດ digest ຄົງທີ່"
|
||||
ContainerImageCheckAvailable: "ຄອນເທນເນີ {{ .name }} ມີອັບເດດ: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "ບໍ່ພົບອັບເດດສຳລັບຄອນເທນເນີ {{ .name }}"
|
||||
ContainerImageCheckChanged: "ຄອນເທນເນີ {{ .name }} ຖືກລຶບ ຫຼື ອິມເມຈປ່ຽນແປງ; ບໍ່ໄດ້ບັນທຶກຜົນ"
|
||||
ContainerImageCheckSummary: "ກວດສອບສິ້ນສຸດ: ກວດແລ້ວ {{ .checked }}, ອັບເກຣດໄດ້ {{ .updates }}, ລົ້ມເຫຼວ {{ .failed }}, ຂ້າມ {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "ກວດສອບ {{ .count }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
|
||||
|
||||
ErrImageCheckProxyRequired: "ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ."
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Sumber yang diminta {{ .err }} tidak dapat ditemui'
|
||||
|
||||
# lompat pantas
|
||||
ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas'
|
||||
ErrMaxQuickJump: 'Maksimum 4 pintasan pantas'
|
||||
ErrMaxQuickJump: 'Maksimum 5 pintasan pantas'
|
||||
|
||||
# umum
|
||||
ErrShutDown: 'Arahan diberhentikan manual'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
|
||||
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
|
||||
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
|
||||
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
|
||||
ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun'
|
||||
ErrAgentAccountSingleInitialModel: 'Penyedia ini membenarkan paling banyak satu model dikonfigurasi'
|
||||
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
|
||||
ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya'
|
||||
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Fail sijil tidak wujud'
|
||||
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
|
||||
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
|
||||
ErrOpenrestyInstall: "Sila pasang OpenResty daripada gedung aplikasi terlebih dahulu"
|
||||
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
|
||||
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
|
||||
dnsAccount: 'DNS automatik'
|
||||
@@ -735,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"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuh
|
||||
AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "Semak kemas kini imej"
|
||||
ContainerImageCheckScope: "Menyemak kesemua {{ .count }} kontena pada nod ini tanpa mengira pembahagian halaman atau penapis"
|
||||
ContainerImageCheckItem: "Menyemak kontena {{ .name }}, imej {{ .image }}"
|
||||
ContainerImageCheckFailed: "Semakan kontena {{ .name }} gagal: {{ .error }}; keputusan terdahulu yang sepadan dengan imej semasa dikekalkan"
|
||||
ContainerImageCheckSkipped: "Melangkau {{ .name }}: bukan imej Docker Hub bertag, imej setempat atau digest ditetapkan"
|
||||
ContainerImageCheckAvailable: "Kontena {{ .name }} mempunyai kemas kini: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "Tiada kemas kini ditemui untuk kontena {{ .name }}"
|
||||
ContainerImageCheckChanged: "Kontena {{ .name }} telah dipadam atau imejnya berubah; keputusan tidak disimpan"
|
||||
ContainerImageCheckSummary: "Semakan selesai: disemak {{ .checked }}, boleh dikemas kini {{ .updates }}, gagal {{ .failed }}, dilangkau {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "Semakan {{ .count }} kontena gagal; lihat log tugas"
|
||||
|
||||
ErrImageCheckProxyRequired: "Untuk menyemak kemas kini imej dalam persekitaran semasa, sila konfigurasikan pelayan proksi terlebih dahulu."
|
||||
|
||||
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'O recurso solicitado {{ .err }} não pôde ser encontrado'
|
||||
|
||||
# atalho rápido
|
||||
ErrMinQuickJump: 'Adicione pelo menos um atalho rápido'
|
||||
ErrMaxQuickJump: 'Máximo 4 atalhos rápidos'
|
||||
ErrMaxQuickJump: 'Máximo 5 atalhos rápidos'
|
||||
|
||||
# comum
|
||||
ErrShutDown: 'Comando parado manualmente'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
|
||||
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
|
||||
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
|
||||
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
|
||||
ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta'
|
||||
ErrAgentAccountSingleInitialModel: 'Este provedor permite configurar no máximo um modelo'
|
||||
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
|
||||
ErrAgentVerifyModelInUse: 'Este modelo é usado para verificar a conta. Selecione outro modelo de verificação antes de excluí-lo'
|
||||
ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'O arquivo de certificado não existe'
|
||||
ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada'
|
||||
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ou EabHmacKey não podem estar em branco'
|
||||
ErrOpenrestyInstall: "Instale primeiro o OpenResty pela loja de aplicativos"
|
||||
ErrOpenrestyNotFound: 'O modo HTTP requer o OpenResty instalado'
|
||||
ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]'
|
||||
dnsAccount: 'DNS Automático'
|
||||
@@ -735,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"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração co
|
||||
AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "Verificar atualizações de imagens"
|
||||
ContainerImageCheckScope: "Verificando todos os {{ .count }} contêineres deste nó, independentemente da paginação e dos filtros"
|
||||
ContainerImageCheckItem: "Verificando o contêiner {{ .name }}, imagem {{ .image }}"
|
||||
ContainerImageCheckFailed: "Falha ao verificar o contêiner {{ .name }}: {{ .error }}; os resultados anteriores correspondentes à imagem atual serão mantidos"
|
||||
ContainerImageCheckSkipped: "Ignorando {{ .name }}: não é uma imagem do Docker Hub com tag, é uma imagem local ou tem digest fixo"
|
||||
ContainerImageCheckAvailable: "O contêiner {{ .name }} tem uma atualização: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "Nenhuma atualização encontrada para o contêiner {{ .name }}"
|
||||
ContainerImageCheckChanged: "O contêiner {{ .name }} foi removido ou sua imagem mudou; o resultado não foi salvo"
|
||||
ContainerImageCheckSummary: "Verificação concluída: verificados {{ .checked }}, atualizáveis {{ .updates }}, falhas {{ .failed }}, ignorados {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "Falha na verificação de {{ .count }} contêineres; consulte o log da tarefa"
|
||||
|
||||
ErrImageCheckProxyRequired: "Para verificar atualizações de imagens no ambiente atual, configure primeiro um servidor proxy."
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Запрошенный ресурс {{ .err }} не най
|
||||
|
||||
# быстрый переход
|
||||
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
|
||||
ErrMaxQuickJump: 'Не более 4 быстрых ссылок'
|
||||
ErrMaxQuickJump: 'Не более 5 быстрых ссылок'
|
||||
|
||||
# общее
|
||||
ErrShutDown: 'Команда остановлена вручную'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер
|
||||
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
||||
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
||||
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
|
||||
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
|
||||
ErrAgentAccountSingleInitialModel: 'Для этого провайдера можно настроить не более одной модели'
|
||||
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
||||
ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки'
|
||||
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Файл сертификата не существ
|
||||
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
|
||||
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
|
||||
ErrOpenrestyInstall: "Сначала установите OpenResty из магазина приложений"
|
||||
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
|
||||
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
|
||||
dnsAccount: 'DNS Авто'
|
||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не с
|
||||
# виртуальная машина
|
||||
ErrVMNotFound: "Виртуальная машина не существует"
|
||||
ErrVMAlreadyExists: "Виртуальная машина уже существует"
|
||||
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку."
|
||||
ErrVMCountLimit: "В текущей редакции можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на старшую редакцию и повторите попытку."
|
||||
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
|
||||
ErrVMHelperNotFound: "1panel-kvm не найден"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Эти монтирования не поддержив
|
||||
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "Проверить обновления образов"
|
||||
ContainerImageCheckScope: "Проверка всех контейнеров на текущем узле ({{ .count }}) независимо от страниц и фильтров"
|
||||
ContainerImageCheckItem: "Проверка контейнера {{ .name }}, образ {{ .image }}"
|
||||
ContainerImageCheckFailed: "Ошибка проверки контейнера {{ .name }}: {{ .error }}; сохранены предыдущие результаты, соответствующие текущему образу"
|
||||
ContainerImageCheckSkipped: "Пропуск {{ .name }}: это не образ Docker Hub с тегом, локальный образ или образ с фиксированным digest"
|
||||
ContainerImageCheckAvailable: "Доступно обновление контейнера {{ .name }}: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "Обновлений для контейнера {{ .name }} не найдено"
|
||||
ContainerImageCheckChanged: "Контейнер {{ .name }} удалён или его образ изменился; результат не сохранён"
|
||||
ContainerImageCheckSummary: "Проверка завершена: проверено {{ .checked }}, доступны обновления {{ .updates }}, ошибок {{ .failed }}, пропущено {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "Не удалось проверить контейнеры: {{ .count }}. Подробности в журнале задачи"
|
||||
|
||||
ErrImageCheckProxyRequired: "Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер."
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'İstenen kaynak {{ .err }} bulunamadı'
|
||||
|
||||
# hızlı geçiş
|
||||
ErrMinQuickJump: 'En az bir hızlı erişim ekle'
|
||||
ErrMaxQuickJump: '4 hızlı erişim hakkı var'
|
||||
ErrMaxQuickJump: '5 hızlı erişim hakkı var'
|
||||
|
||||
# genel
|
||||
ErrShutDown: 'Komut manuel durduruldu'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
|
||||
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
|
||||
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
|
||||
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
|
||||
ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir'
|
||||
ErrAgentAccountSingleInitialModel: 'Bu sağlayıcı için en fazla bir model yapılandırılabilir'
|
||||
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
|
||||
ErrAgentVerifyModelInUse: 'Bu model hesap doğrulaması için kullanılıyor. Silmeden önce başka bir doğrulama modeli seçin'
|
||||
ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Sertifika dosyası mevcut değil'
|
||||
ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız'
|
||||
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
|
||||
ErrOpenrestyInstall: "Lütfen önce uygulama mağazasından OpenResty yükleyin"
|
||||
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
|
||||
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
|
||||
dnsAccount: 'DNS Otomatik'
|
||||
@@ -735,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ı"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygul
|
||||
AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu."
|
||||
|
||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
||||
ContainerImageCheckUpdate: "İmaj güncellemelerini denetle"
|
||||
ContainerImageCheckScope: "Sayfalama ve filtrelerden bağımsız olarak bu düğümdeki {{ .count }} konteynerin tümü denetleniyor"
|
||||
ContainerImageCheckItem: "Konteyner {{ .name }}, imaj {{ .image }} denetleniyor"
|
||||
ContainerImageCheckFailed: "{{ .name }} konteyneri denetlenemedi: {{ .error }}; mevcut imajla eşleşen önceki sonuçlar korunuyor"
|
||||
ContainerImageCheckSkipped: "{{ .name }} atlanıyor: etiketli bir Docker Hub imajı değil, yerel bir imaj veya digest değeri sabitlenmiş"
|
||||
ContainerImageCheckAvailable: "{{ .name }} konteyneri için güncelleme mevcut: {{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "{{ .name }} konteyneri için güncelleme bulunamadı"
|
||||
ContainerImageCheckChanged: "{{ .name }} konteyneri silindi veya imajı değişti; sonuç kaydedilmedi"
|
||||
ContainerImageCheckSummary: "Denetim tamamlandı: denetlenen {{ .checked }}, güncellenebilir {{ .updates }}, başarısız {{ .failed }}, atlanan {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "{{ .count }} konteyner denetlenemedi; görev günlüğünü inceleyin"
|
||||
|
||||
ErrImageCheckProxyRequired: "Geçerli çalışma ortamında imaj güncellemelerini kontrol etmek için önce bir proxy sunucusu yapılandırın."
|
||||
|
||||
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '無法找到要求的資源{{ .err }}'
|
||||
|
||||
# 快速跳轉
|
||||
ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
|
||||
ErrMaxQuickJump: '最多可設定四個快速跳轉入口!'
|
||||
ErrMaxQuickJump: '最多可設定五個快速跳轉入口!'
|
||||
|
||||
# 通用
|
||||
ErrShutDown: '指令被手動結束!'
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
|
||||
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
|
||||
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
|
||||
ErrAgentAccountModelsRequired: '請至少配置一個模型'
|
||||
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
|
||||
ErrAgentAccountSingleInitialModel: '目前提供商最多可設定一個模型'
|
||||
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
|
||||
ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除'
|
||||
ErrAgentFallbackModelDuplicate: '備用模型不能重複'
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '憑證檔案不存在'
|
||||
ErrSSLKeyFormat: '私鑰檔案驗證失敗'
|
||||
ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空'
|
||||
ErrOpenrestyInstall: "請先在應用程式商店安裝 OpenResty"
|
||||
ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty'
|
||||
ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]'
|
||||
dnsAccount: 'DNS 自動'
|
||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
|
||||
# 虛擬機
|
||||
ErrVMNotFound: "虛擬機不存在"
|
||||
ErrVMAlreadyExists: "虛擬機已存在"
|
||||
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
|
||||
ErrVMCountLimit: "目前版本最多可建立 {{ .limit }} 台虛擬機。 請刪除現有虛擬機或升級版本後重試。"
|
||||
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
|
||||
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應
|
||||
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
|
||||
|
||||
ContainerUpgradeWaiting: "等待容器升級任務執行名額"
|
||||
ContainerImageCheckUpdate: "檢查更新映像"
|
||||
ContainerImageCheckScope: "檢查目前節點全部 {{ .count }} 個容器,不受分頁和篩選影響"
|
||||
ContainerImageCheckItem: "檢查容器 {{ .name }},映像 {{ .image }}"
|
||||
ContainerImageCheckFailed: "容器 {{ .name }} 檢查失敗:{{ .error }};保留符合目前映像的上次結果"
|
||||
ContainerImageCheckSkipped: "略過 {{ .name }}:非 Docker Hub 標籤映像、本機映像或固定 digest"
|
||||
ContainerImageCheckAvailable: "容器 {{ .name }} 可升級:{{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "容器 {{ .name }} 未發現更新"
|
||||
ContainerImageCheckChanged: "容器 {{ .name }} 已刪除或映像已變更,未儲存本次結果"
|
||||
ContainerImageCheckSummary: "檢查結束:成功 {{ .checked }},可升級 {{ .updates }},失敗 {{ .failed }},略過 {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "{{ .count }} 個容器檢查失敗,請查看任務日誌"
|
||||
|
||||
ErrImageCheckProxyRequired: "目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器"
|
||||
|
||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: "无法找到请求的资源 {{ .err }}"
|
||||
|
||||
# 快速跳转
|
||||
ErrMinQuickJump: "至少设置 1 个快速跳转入口"
|
||||
ErrMaxQuickJump: "最多设置 4 个快速跳转入口"
|
||||
ErrMaxQuickJump: "最多设置 5 个快速跳转入口"
|
||||
|
||||
# 通用
|
||||
ErrShutDown: "命令已手动终止"
|
||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商"
|
||||
ErrAgentAccountRequired: "请选择智能体账号后重试"
|
||||
ErrAgentAccountNotVerified: "账号未通过验证"
|
||||
ErrAgentAccountModelsRequired: "请至少配置一个模型"
|
||||
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
|
||||
ErrAgentAccountSingleInitialModel: '当前提供商最多配置一个模型'
|
||||
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
|
||||
ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除"
|
||||
ErrAgentFallbackModelDuplicate: "备用模型不能重复"
|
||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '证书文件不存在'
|
||||
ErrSSLKeyFormat: '私钥文件校验失败'
|
||||
ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空'
|
||||
ErrOpenrestyInstall: "请先在应用商店安装 OpenResty"
|
||||
ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty'
|
||||
ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]'
|
||||
dnsAccount: "DNS 自动"
|
||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
|
||||
# 虚拟机
|
||||
ErrVMNotFound: "虚拟机不存在"
|
||||
ErrVMAlreadyExists: "虚拟机已存在"
|
||||
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
|
||||
ErrVMCountLimit: "当前版本最多可创建 {{ .limit }} 台虚拟机。 请删除已有虚拟机或升级版本后重试。"
|
||||
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
|
||||
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应
|
||||
AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。"
|
||||
|
||||
ContainerUpgradeWaiting: "等待容器升级任务执行名额"
|
||||
ContainerImageCheckUpdate: "检查更新镜像"
|
||||
ContainerImageCheckScope: "检查当前节点全部 {{ .count }} 个容器,不受分页和筛选影响"
|
||||
ContainerImageCheckItem: "检查容器 {{ .name }},镜像 {{ .image }}"
|
||||
ContainerImageCheckFailed: "容器 {{ .name }} 检查失败:{{ .error }};保留匹配当前镜像的上次结果"
|
||||
ContainerImageCheckSkipped: "跳过 {{ .name }}:非 Docker Hub 标签镜像、本地镜像或固定 digest"
|
||||
ContainerImageCheckAvailable: "容器 {{ .name }} 可升级:{{ .image }} → {{ .target }}"
|
||||
ContainerImageCheckCurrent: "容器 {{ .name }} 未发现更新"
|
||||
ContainerImageCheckChanged: "容器 {{ .name }} 已删除或镜像发生变化,未保存本次结果"
|
||||
ContainerImageCheckSummary: "检查结束:成功 {{ .checked }},可升级 {{ .updates }},失败 {{ .failed }},跳过 {{ .skipped }}"
|
||||
ContainerImageCheckFailures: "{{ .count }} 个容器检查失败,请查看任务日志"
|
||||
|
||||
ErrImageCheckProxyRequired: "当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器"
|
||||
|
||||
@@ -10,6 +10,12 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
global.LOG.Info("recover interrupted model download tasks start")
|
||||
if err := service.NewIModelDownloaderService().RecoverInterruptedTasks(); err != nil {
|
||||
global.LOG.Warnf("recover interrupted model download tasks failed, err: %v", err)
|
||||
}
|
||||
global.LOG.Info("recover interrupted model download tasks done")
|
||||
|
||||
go syncApp()
|
||||
go syncInstalledApp()
|
||||
go syncRuntime()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,10 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddQuickDirectory,
|
||||
migrations.AddLocalModelAccountLink,
|
||||
migrations.AddOllamaModelAccountSync,
|
||||
migrations.AddContainerImageCheckUpdates,
|
||||
migrations.AddModelQuickJump,
|
||||
migrations.MigrateModelDownloader,
|
||||
migrations.MigrateVMDatabase,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var AddContainerImageCheckUpdates = &gormigrate.Migration{
|
||||
ID: "20261028-container-image-updates",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.ContainerImageCheckUpdate{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("key = ?", "ContainerImageUpdateEnabled").FirstOrCreate(&model.Setting{Key: "ContainerImageUpdateEnabled", Value: "Disable"}).Error
|
||||
},
|
||||
}
|
||||
@@ -1939,3 +1939,16 @@ var AddOllamaModelAccountSync = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddModelQuickJump = &gormigrate.Migration{
|
||||
ID: "20261030-add-model-quick-jump",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "ModelServices").FirstOrCreate(&model.QuickJump{
|
||||
Name: "ModelServices",
|
||||
Title: "aiTools.model.model",
|
||||
Recommend: 0,
|
||||
IsShow: true,
|
||||
Router: "/ai/model",
|
||||
}).Error
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var MigrateModelDownloader = &gormigrate.Migration{
|
||||
ID: "20261102-migrate-model-downloader",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
return migrateModelDownloader(db, filepath.Join(global.Dir.DbDir, "xpack.db"))
|
||||
},
|
||||
}
|
||||
|
||||
func migrateModelDownloader(db *gorm.DB, legacyPath string) error {
|
||||
var legacy *gorm.DB
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
source := url.URL{Scheme: "file", Path: legacyPath, RawQuery: "mode=ro"}
|
||||
legacy, err = gorm.Open(sqlite.Open(source.String()), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB, err := legacy.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.ModelDownloadTask{}, &model.FeaturedModel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
resetVersion := false
|
||||
if legacy != nil {
|
||||
if legacy.Migrator().HasTable(&model.ModelDownloadTask{}) {
|
||||
var tasks []model.ModelDownloadTask
|
||||
if err := legacy.FindInBatches(&tasks, 100, func(_ *gorm.DB, _ int) error {
|
||||
for i := range tasks {
|
||||
if tasks[i].DownloadType == "" {
|
||||
tasks[i].DownloadType = "all"
|
||||
}
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&tasks).Error
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if legacy.Migrator().HasTable(&model.FeaturedModel{}) {
|
||||
resetVersion = !legacy.Migrator().HasColumn(&model.FeaturedModel{}, "Sources") || !legacy.Migrator().HasColumn(&model.FeaturedModel{}, "SourceModelIDs")
|
||||
var models []model.FeaturedModel
|
||||
if err := legacy.FindInBatches(&models, 100, func(_ *gorm.DB, _ int) error {
|
||||
for i := range models {
|
||||
if len(models[i].Sources) == 0 {
|
||||
models[i].Sources = []string{"modelscope"}
|
||||
}
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
resetVersion = true
|
||||
}
|
||||
if legacy.Migrator().HasTable("agent_settings") {
|
||||
var settings []model.Setting
|
||||
keys := []string{"ModelDownloaderDir", "ModelDownloaderHFEndpoint", "ModelDownloaderHFToken", "ModelDownloaderModelScopeEndpoint", "ModelDownloaderModelScopeToken", "FeaturedModelsSync", "FeaturedModelsVersion"}
|
||||
if err := legacy.Table("agent_settings").Where("key IN ?", keys).Find(&settings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, setting := range settings {
|
||||
if setting.Key == "FeaturedModelsVersion" && resetVersion {
|
||||
setting.Value = ""
|
||||
}
|
||||
if err := tx.Where("key = ?", setting.Key).FirstOrCreate(&model.Setting{Key: setting.Key, Value: setting.Value, About: setting.About}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&model.FeaturedModel{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Model(&model.Setting{}).Where("key = ?", "FeaturedModelsVersion").Update("value", "").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
models := []model.FeaturedModel{
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.6-35B-A3B", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 67},
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.6-35B-A3B-FP8", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 35},
|
||||
{Family: "Qwen", RepoID: "nvidia/Qwen3.6-35B-A3B-NVFP4", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 22},
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-27B", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 55},
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-27B-FP8", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 28},
|
||||
{Family: "Qwen", RepoID: "nvidia/Qwen3.8-27B-NVFP4", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 22},
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-Flash-Next", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 335},
|
||||
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-Flash-Next-FP8", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 172},
|
||||
{Family: "Qwen", RepoID: "nvidia/Qwen3.8-Flash-Next-FP4-Local", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 130},
|
||||
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4-Flash-0731", Architecture: "MoE", BackboneParams: "284B", ActiveParams: "13B", Engram: "N/A", WeightGB: 167},
|
||||
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp", Architecture: "MoE", BackboneParams: "305B", ActiveParams: "undisclosed", Engram: "N/A", WeightGB: 167},
|
||||
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4.1-Flash-1017", Architecture: "MoE", BackboneParams: "552B", ActiveParams: "8B/16B", Engram: "196B", WeightGB: 510},
|
||||
{Family: "GLM", RepoID: "zai-org/GLM-5.3-Flash", Architecture: "MoE", BackboneParams: "320B", ActiveParams: "18B", Engram: "N/A", WeightGB: 306},
|
||||
{Family: "GLM", RepoID: "nvidia/GLM-5.3-Flash-FP4-Local", Architecture: "MoE", BackboneParams: "320B", ActiveParams: "18B", Engram: "N/A", WeightGB: 185},
|
||||
}
|
||||
for i := range models {
|
||||
models[i].Key = models[i].RepoID
|
||||
models[i].Sort = i + 1
|
||||
models[i].Description = "{}"
|
||||
models[i].Sources = []string{"modelscope"}
|
||||
}
|
||||
return tx.Create(&models).Error
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func Routers() *gin.Engine {
|
||||
}
|
||||
Router = gin.Default()
|
||||
Router.Use(i18n.UseI18n())
|
||||
Router.Use(middleware.MCPErrorMetadata())
|
||||
registerMCPRuntime(Router)
|
||||
|
||||
PrivateGroup := Router.Group("/api/v2")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func MCPErrorMetadata() gin.HandlerFunc {
|
||||
return helper.WithErrorMetadata(func(c *gin.Context, _ int, _ string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(c.Request, c.Writer.Header(), err)
|
||||
})
|
||||
}
|
||||
@@ -60,6 +60,9 @@ func OperationResolveMeta() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
|
||||
if strings.HasPrefix(reqPath, "/xpack/model/downloader/") {
|
||||
reqPath = strings.Replace(reqPath, "/xpack/model/downloader/", "/ai/model/downloader/", 1)
|
||||
}
|
||||
meta, ok := metaMap[reqPath]
|
||||
if !ok || len(meta.BeforeFunctions) == 0 {
|
||||
c.Next()
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
func commonGroups() []CommonRouter {
|
||||
return []CommonRouter{
|
||||
&VMRouter{},
|
||||
&DashboardRouter{},
|
||||
&HostRouter{},
|
||||
&ContainerRouter{},
|
||||
@@ -23,6 +24,7 @@ func commonGroups() []CommonRouter {
|
||||
&ProcessRouter{},
|
||||
&WebsiteCARouter{},
|
||||
&AIToolsRouter{},
|
||||
&ModelDownloaderRouter{},
|
||||
&GroupRouter{},
|
||||
&AlertRouter{},
|
||||
&RuntimeDiagnosticsRouter{},
|
||||
|
||||
@@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
baRouter.POST("", baseApi.ContainerCreate)
|
||||
baRouter.POST("/update", baseApi.ContainerUpdate)
|
||||
baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
|
||||
baRouter.GET("/image-updates", baseApi.GetContainerImageCheckUpdateStatus)
|
||||
baRouter.POST("/image-updates/check", baseApi.ContainerImageCheckUpdate)
|
||||
baRouter.POST("/image-updates/schedule", baseApi.SetContainerImageCheckUpdateSchedule)
|
||||
baRouter.POST("/info", baseApi.ContainerInfo)
|
||||
baRouter.POST("/search", baseApi.SearchContainer)
|
||||
baRouter.POST("/list", baseApi.ListContainer)
|
||||
@@ -64,6 +67,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
baRouter.POST("/template", baseApi.CreateComposeTemplate)
|
||||
baRouter.POST("/template/del", baseApi.DeleteComposeTemplate)
|
||||
|
||||
baRouter.POST("/image/remote/search", baseApi.SearchRemoteImages)
|
||||
baRouter.GET("/image", baseApi.ListImage)
|
||||
baRouter.GET("/image/all", baseApi.ListAllImage)
|
||||
baRouter.POST("/image/search", baseApi.SearchImage)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ModelDownloaderRouter struct{}
|
||||
|
||||
func (m *ModelDownloaderRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
baseApi := v2.ApiGroupApp.BaseApi
|
||||
for _, prefix := range []string{"ai/model/downloader", "xpack/model/downloader"} {
|
||||
router := Router.Group(prefix)
|
||||
router.POST("/featured/search", baseApi.PageFeaturedModels)
|
||||
router.POST("/featured/sync", baseApi.SyncFeaturedModels)
|
||||
router.POST("/featured/sync/status", baseApi.UpdateFeaturedModelsSync)
|
||||
router.POST("/hf/search", baseApi.SearchHFModels)
|
||||
router.POST("/hf/info", baseApi.GetHFModelInfo)
|
||||
router.POST("/hf/download", baseApi.StartHFModelDownload)
|
||||
router.POST("/modelscope/search", baseApi.SearchModelScopeModels)
|
||||
router.POST("/modelscope/info", baseApi.GetModelScopeModelInfo)
|
||||
router.POST("/modelscope/download", baseApi.StartModelScopeModelDownload)
|
||||
router.GET("/settings", baseApi.GetModelDownloaderSettings)
|
||||
router.POST("/settings", baseApi.UpdateModelDownloaderSettings)
|
||||
router.POST("/tasks/search", baseApi.PageModelDownloadTasks)
|
||||
router.POST("/tasks/cancel", baseApi.CancelModelDownloadTask)
|
||||
router.POST("/tasks/retry", baseApi.RetryModelDownloadTask)
|
||||
router.POST("/tasks/remove", baseApi.RemoveModelDownloadTask)
|
||||
router.POST("/local/search", baseApi.PageLocalDownloadedModels)
|
||||
router.POST("/local/delete", baseApi.DeleteLocalDownloadedModel)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -72,7 +72,8 @@ type Device struct {
|
||||
}
|
||||
|
||||
type Basic struct {
|
||||
UUID string `json:"uuid"`
|
||||
DrmDevice string `json:"-"`
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
DeviceID int `json:"deviceID"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
|
||||
@@ -93,6 +93,7 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
||||
xpu := Device{
|
||||
ProcessStatus: "unavailable",
|
||||
Basic: Basic{
|
||||
DrmDevice: device.DrmDevice,
|
||||
UUID: device.UUID,
|
||||
DeviceID: device.DeviceID,
|
||||
DeviceName: device.DeviceName,
|
||||
@@ -137,6 +138,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
||||
return
|
||||
}
|
||||
|
||||
if info.DrmDevice != "" {
|
||||
xpu.Basic.DrmDevice = info.DrmDevice
|
||||
}
|
||||
xpu.Basic.DriverVersion = info.DriverVersion
|
||||
|
||||
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
|
||||
|
||||
@@ -202,6 +202,8 @@ func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string
|
||||
}
|
||||
|
||||
func (c Client) PullImageWithProcessAndOptionsContext(ctx context.Context, task *task.Task, imageName string, options image.PullOptions) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 24*time.Hour)
|
||||
defer cancel()
|
||||
out, err := c.cli.ImagePull(ctx, imageName, options)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Package dockerhub checks Docker Hub metadata without downloading image layers.
|
||||
package dockerhub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
)
|
||||
|
||||
const registryURL = "https://registry-1.docker.io"
|
||||
|
||||
// Reference only accepts tagged Docker Hub references. Digest pins are intentional.
|
||||
func Reference(raw string) (string, string, bool) {
|
||||
if strings.HasPrefix(raw, "sha256:") {
|
||||
return "", "", false
|
||||
}
|
||||
ref, err := reference.ParseNormalizedNamed(raw)
|
||||
if err != nil || reference.Domain(ref) != "docker.io" {
|
||||
return "", "", false
|
||||
}
|
||||
if _, pinned := ref.(reference.Digested); pinned {
|
||||
return "", "", false
|
||||
}
|
||||
tagged := reference.TagNameOnly(ref).(reference.NamedTagged)
|
||||
return reference.Path(ref), tagged.Tag(), true
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
HTTP *http.Client
|
||||
repository string
|
||||
token string
|
||||
}
|
||||
|
||||
type Platform struct{ OS, Architecture, Variant string }
|
||||
type Target struct{ Tag, Digest, ConfigDigest string }
|
||||
|
||||
func New(ctx context.Context, repository, username, password string, httpClient *http.Client) (*Client, error) {
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
c := &Client{HTTP: httpClient, repository: repository}
|
||||
endpoint := "https://auth.docker.io/token?service=registry.docker.io&scope=" + url.QueryEscape("repository:"+repository+":pull")
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if username != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Docker Hub authentication: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
var body struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.token = body.Token
|
||||
if c.token == "" {
|
||||
c.token = body.AccessToken
|
||||
}
|
||||
if c.token == "" {
|
||||
return nil, fmt.Errorf("Docker Hub returned an empty token")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, endpoint string, result interface{}) (http.Header, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json")
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Docker Hub metadata: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
err = json.NewDecoder(io.LimitReader(resp.Body, 16<<20)).Decode(result)
|
||||
return resp.Header, err
|
||||
}
|
||||
|
||||
func (c *Client) Tags(ctx context.Context) ([]string, error) {
|
||||
endpoint := registryURL + "/v2/" + c.repository + "/tags/list?n=1000"
|
||||
var tags []string
|
||||
for page := 0; page < 100; page++ {
|
||||
var body struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
headers, err := c.get(ctx, endpoint, &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, body.Tags...)
|
||||
link := headers.Get("Link")
|
||||
if link == "" {
|
||||
return tags, nil
|
||||
}
|
||||
start, end := strings.Index(link, "<"), strings.Index(link, ">")
|
||||
if start < 0 || end <= start {
|
||||
return nil, fmt.Errorf("invalid Docker Hub pagination")
|
||||
}
|
||||
base, _ := url.Parse(registryURL)
|
||||
next, err := base.Parse(link[start+1 : end])
|
||||
if err != nil || next.Scheme != "https" || next.Host != base.Host || next.Path != "/v2/"+c.repository+"/tags/list" {
|
||||
return nil, fmt.Errorf("invalid Docker Hub pagination URL")
|
||||
}
|
||||
endpoint = next.String()
|
||||
}
|
||||
return nil, fmt.Errorf("Docker Hub tag pagination limit exceeded")
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Config struct {
|
||||
Digest string `json:"digest"`
|
||||
} `json:"config"`
|
||||
Manifests []struct {
|
||||
Digest string `json:"digest"`
|
||||
Platform struct {
|
||||
OS string `json:"os"`
|
||||
Architecture string `json:"architecture"`
|
||||
Variant string `json:"variant"`
|
||||
} `json:"platform"`
|
||||
} `json:"manifests"`
|
||||
}
|
||||
|
||||
func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Target, bool, error) {
|
||||
var m manifest
|
||||
headers, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(tag), &m)
|
||||
if err != nil {
|
||||
return Target{}, false, err
|
||||
}
|
||||
digest := headers.Get("Docker-Content-Digest")
|
||||
if len(m.Manifests) > 0 {
|
||||
digest = ""
|
||||
for _, item := range m.Manifests {
|
||||
if item.Platform.OS == platform.OS && item.Platform.Architecture == platform.Architecture && normalizedVariant(platform.Architecture, item.Platform.Variant) == normalizedVariant(platform.Architecture, platform.Variant) {
|
||||
digest = item.Digest
|
||||
break
|
||||
}
|
||||
}
|
||||
if digest == "" {
|
||||
return Target{}, false, nil
|
||||
}
|
||||
m = manifest{}
|
||||
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(digest), &m); err != nil {
|
||||
return Target{}, false, err
|
||||
}
|
||||
}
|
||||
if m.Config.Digest == "" {
|
||||
return Target{}, false, fmt.Errorf("unsupported Docker Hub manifest")
|
||||
}
|
||||
// Validate single-platform manifests too; the config digest is the Docker Image ID.
|
||||
var config struct {
|
||||
OS string `json:"os"`
|
||||
Architecture string `json:"architecture"`
|
||||
Variant string `json:"variant"`
|
||||
}
|
||||
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/blobs/"+url.PathEscape(m.Config.Digest), &config); err != nil {
|
||||
return Target{}, false, err
|
||||
}
|
||||
if config.OS != platform.OS || config.Architecture != platform.Architecture || (platform.Variant != "" && config.Variant != "" && config.Variant != platform.Variant) {
|
||||
return Target{}, false, nil
|
||||
}
|
||||
return Target{Tag: tag, Digest: digest, ConfigDigest: m.Config.Digest}, true, nil
|
||||
}
|
||||
|
||||
var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(-[a-zA-Z][a-zA-Z0-9_.-]*)?$`)
|
||||
var prereleasePattern = regexp.MustCompile(`(?i)(^|[-_.])(alpha|beta|rc|dev|nightly|edge|test|unstable|preview)([0-9_.-]|$)`)
|
||||
|
||||
type version struct {
|
||||
numbers [3]uint64
|
||||
parts int
|
||||
variant string
|
||||
}
|
||||
|
||||
func parseVersion(tag string) (version, bool) {
|
||||
m := versionPattern.FindStringSubmatch(tag)
|
||||
if m == nil || prereleasePattern.MatchString(m[4]) {
|
||||
return version{}, false
|
||||
}
|
||||
v := version{parts: 2, variant: m[4]}
|
||||
for i := 0; i < 3; i++ {
|
||||
if m[i+1] != "" {
|
||||
n, err := strconv.ParseUint(m[i+1], 10, 64)
|
||||
if err != nil {
|
||||
return version{}, false
|
||||
}
|
||||
v.numbers[i] = n
|
||||
}
|
||||
}
|
||||
if m[3] != "" {
|
||||
v.parts = 3
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
func greater(a, b version) bool {
|
||||
for i := 0; i < 3; i++ {
|
||||
if a.numbers[i] != b.numbers[i] {
|
||||
return a.numbers[i] > b.numbers[i]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Candidates preserves the current tag's major version, variant and precision. Unknown/floating
|
||||
// tags are checked only for content changes, never guessed from upload dates.
|
||||
func Candidates(current string, tags []string) []string {
|
||||
baseline, ok := parseVersion(current)
|
||||
if !ok {
|
||||
return []string{current}
|
||||
}
|
||||
type candidate struct {
|
||||
tag string
|
||||
version version
|
||||
}
|
||||
candidates := []candidate{}
|
||||
seen := map[string]bool{}
|
||||
for _, tag := range tags {
|
||||
v, valid := parseVersion(tag)
|
||||
if valid && v.numbers[0] == baseline.numbers[0] && v.parts == baseline.parts && v.variant == baseline.variant && greater(v, baseline) && !seen[tag] {
|
||||
candidates = append(candidates, candidate{tag: tag, version: v})
|
||||
seen[tag] = true
|
||||
}
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
a, b := candidates[i], candidates[j]
|
||||
if a.version.numbers == b.version.numbers {
|
||||
return a.tag < b.tag
|
||||
}
|
||||
return greater(a.version, b.version)
|
||||
})
|
||||
result := make([]string, 0, len(candidates)+1)
|
||||
for _, candidate := range candidates {
|
||||
result = append(result, candidate.tag)
|
||||
}
|
||||
|
||||
return append(result, current)
|
||||
}
|
||||
|
||||
func NeedsTags(tag string) bool { _, ok := parseVersion(tag); return ok }
|
||||
|
||||
func normalizedVariant(architecture, variant string) string {
|
||||
if architecture == "arm64" && variant == "" {
|
||||
return "v8"
|
||||
}
|
||||
return variant
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user