mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-02 16:05:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
superpowers
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.codex/
|
.codex/
|
||||||
|
|
||||||
|
/agent/utils/vm/helper/bin/1panel-kvm*
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ build_core_on_linux:
|
|||||||
cd $(CORE_PATH) \
|
cd $(CORE_PATH) \
|
||||||
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN)
|
&& 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) \
|
cd $(AGENT_PATH) \
|
||||||
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN)
|
&& 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) \
|
cd $(CORE_PATH) \
|
||||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN)
|
&& 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) \
|
cd $(AGENT_PATH) \
|
||||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN)
|
&& 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,12 +11,14 @@ var ApiGroupApp = new(ApiGroup)
|
|||||||
type BaseApi struct{}
|
type BaseApi struct{}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
vmService = service.NewIVMService()
|
||||||
dashboardService = service.NewIDashboardService()
|
dashboardService = service.NewIDashboardService()
|
||||||
|
|
||||||
appService = service.NewIAppService()
|
appService = service.NewIAppService()
|
||||||
appInstallService = service.NewIAppInstalledService()
|
appInstallService = service.NewIAppInstalledService()
|
||||||
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
|
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
|
||||||
|
|
||||||
|
modelDownloaderService = service.NewIModelDownloaderService()
|
||||||
aiToolService = service.NewIAIToolService()
|
aiToolService = service.NewIAIToolService()
|
||||||
mcpServerService = service.NewIMcpServerService()
|
mcpServerService = service.NewIMcpServerService()
|
||||||
tensorrtLLMService = service.NewITensorRTLLMService()
|
tensorrtLLMService = service.NewITensorRTLLMService()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -23,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
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{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: "",
|
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) {
|
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{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
ErrorCode: businessCode,
|
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{}) {
|
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{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -209,3 +208,26 @@ func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
|||||||
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
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
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"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/dto"
|
||||||
|
_ "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" // Swagger response schema.
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,3 +229,30 @@ func (b *BaseApi) ImageLoad(c *gin.Context) {
|
|||||||
|
|
||||||
helper.Success(c)
|
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 {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
list := tensorrtLLMService.Page(req)
|
list, err := tensorrtLLMService.Page(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := tensorrtLLMService.Delete(req.ID)
|
err := tensorrtLLMService.Delete(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -318,6 +318,7 @@ type AgentAccountModelDiscoverReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelCreateReq struct {
|
type AgentAccountModelCreateReq struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model AgentAccountModel `json:"model" validate:"required"`
|
Model AgentAccountModel `json:"model" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -328,6 +329,7 @@ type AgentAccountModelUpdateReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelDeleteReq struct {
|
type AgentAccountModelDeleteReq struct {
|
||||||
|
DeleteFiles bool `json:"deleteFiles"`
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
RecordID uint `json:"recordId" validate:"required"`
|
RecordID uint `json:"recordId" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -358,6 +360,7 @@ type AgentAccountUpdateReq struct {
|
|||||||
ValidateAvailability *bool `json:"validateAvailability"`
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
SyncAgents bool `json:"syncAgents"`
|
SyncAgents bool `json:"syncAgents"`
|
||||||
|
Models []AgentAccountModel `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountVerifyReq struct {
|
type AgentAccountVerifyReq struct {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ type InspectReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContainerInfo struct {
|
type ContainerInfo struct {
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
TargetImage string `json:"targetImage"`
|
||||||
|
HasUpdate bool `json:"hasUpdate"`
|
||||||
ContainerID string `json:"containerID"`
|
ContainerID string `json:"containerID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ImageId string `json:"imageID"`
|
ImageId string `json:"imageID"`
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package dto
|
|||||||
|
|
||||||
import "time"
|
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 {
|
type PageImage struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Name string `json:"name"`
|
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"`
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ type MCPWebsiteBinding struct {
|
|||||||
Upstream string `json:"upstream"`
|
Upstream string `json:"upstream"`
|
||||||
CoreHost string `json:"coreHost"`
|
CoreHost string `json:"coreHost"`
|
||||||
ProxyKey string `json:"proxyKey"`
|
ProxyKey string `json:"proxyKey"`
|
||||||
|
DeleteWebsite bool `json:"deleteWebsite,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type TensorRTLLMSearch struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TensorRTLLMCreate struct {
|
type TensorRTLLMCreate struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
ContainerName string `json:"containerName" validate:"required"`
|
ContainerName string `json:"containerName" validate:"required"`
|
||||||
Version string `json:"version" validate:"required"`
|
Version string `json:"version" validate:"required"`
|
||||||
@@ -27,11 +28,13 @@ type TensorRTLLMUpdate struct {
|
|||||||
|
|
||||||
type TensorRTLLMDelete struct {
|
type TensorRTLLMDelete struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TensorRTLLMOperate struct {
|
type TensorRTLLMOperate struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
ID uint `json:"id" validate:"required"`
|
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 {
|
type DockerConfig struct {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type TagDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledCheck struct {
|
type AppInstalledCheck struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
@@ -107,6 +108,7 @@ type AppDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallDTO struct {
|
type AppInstallDTO struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AppID uint `json:"appID"`
|
AppID uint `json:"appID"`
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ type MCPWebsiteBinding struct {
|
|||||||
Code string `json:"code,omitempty"`
|
Code string `json:"code,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Definitive bool `json:"definitive,omitempty"`
|
Definitive bool `json:"definitive,omitempty"`
|
||||||
|
WebsiteDeleted bool `json:"websiteDeleted,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type TensorRTLLMsRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TensorRTLLMDTO struct {
|
type TensorRTLLMDTO struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
model.TensorRTLLM
|
model.TensorRTLLM
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Model string `json:"model"`
|
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"`
|
Param string `json:"param"`
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
DockerCompose string `json:"dockerCompose" `
|
DockerCompose string `json:"dockerCompose" `
|
||||||
|
TaskID string `json:"taskID" gorm:"-"`
|
||||||
Status string `json:"status" gorm:"not null"`
|
Status string `json:"status" gorm:"not null"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Message string `json:"message"`
|
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"`
|
||||||
|
}
|
||||||
@@ -18,8 +18,9 @@ type IAgentAccountRepo interface {
|
|||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
|
Count() (int64, error)
|
||||||
CountTextByProviders(providers []string) (map[string]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 {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -69,6 +70,12 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
|||||||
return accounts, nil
|
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) {
|
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||||
normalizedProviders := normalizeProviders(providers)
|
normalizedProviders := normalizeProviders(providers)
|
||||||
counts := make(map[string]int64, len(normalizedProviders))
|
counts := make(map[string]int64, len(normalizedProviders))
|
||||||
@@ -99,7 +106,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
|||||||
return counts, nil
|
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 {
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Save(account).Error; err != nil {
|
if err := tx.Save(account).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type IAppInstallRepo interface {
|
|||||||
GetFirst(opts ...DBOption) (model.AppInstall, error)
|
GetFirst(opts ...DBOption) (model.AppInstall, error)
|
||||||
Create(ctx context.Context, install *model.AppInstall) error
|
Create(ctx context.Context, install *model.AppInstall) error
|
||||||
Save(ctx context.Context, install *model.AppInstall) error
|
Save(ctx context.Context, install *model.AppInstall) error
|
||||||
|
UpdateStatus(install *model.AppInstall) error
|
||||||
DeleteBy(opts ...DBOption) error
|
DeleteBy(opts ...DBOption) error
|
||||||
Delete(ctx context.Context, install model.AppInstall) error
|
Delete(ctx context.Context, install model.AppInstall) error
|
||||||
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
|
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
|
||||||
@@ -45,6 +46,11 @@ func NewIAppInstallRepo() IAppInstallRepo {
|
|||||||
return &AppInstallRepo{}
|
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 {
|
func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("app_detail_id in (?)", detailIds)
|
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 {
|
if !withAll && len(quicks) == 0 {
|
||||||
return []model.QuickJump{
|
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: "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: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
|
||||||
{Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"},
|
{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"},
|
{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 {
|
type ITaskRepo interface {
|
||||||
Save(ctx context.Context, task *model.Task) error
|
Save(ctx context.Context, task *model.Task) error
|
||||||
GetFirst(opts ...DBOption) (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)
|
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
||||||
Update(ctx context.Context, task *model.Task) error
|
Update(ctx context.Context, task *model.Task) error
|
||||||
UpdateRunningTaskToFailed(opts ...DBOption) error
|
UpdateRunningTaskToFailed(opts ...DBOption) error
|
||||||
@@ -98,6 +99,12 @@ func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
|
|||||||
return task, nil
|
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) {
|
func (t TaskRepo) Page(page, size int, opts ...DBOption) (int64, []model.Task, error) {
|
||||||
var tasks []model.Task
|
var tasks []model.Task
|
||||||
db := getTaskDb(opts...).Model(&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
|
||||||
|
}
|
||||||
+100
-1
@@ -1105,10 +1105,24 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
requestedVerifyModel := req.VerifyModel
|
||||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||||
|
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
|
||||||
requestedVerifyModel = account.VerifyModel
|
requestedVerifyModel = account.VerifyModel
|
||||||
}
|
}
|
||||||
|
}
|
||||||
verifyModel := ""
|
verifyModel := ""
|
||||||
if account.AppInstallID == 0 || len(models) > 0 {
|
if account.AppInstallID == 0 || len(models) > 0 {
|
||||||
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||||
@@ -1131,7 +1145,16 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
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
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
@@ -1275,6 +1298,31 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
|
|||||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
||||||
return buserr.New("ErrRecordExist")
|
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
|
sortOrder := len(models) + 1
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
@@ -1305,6 +1353,15 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
nextModel.RecordID = req.Model.RecordID
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if item.RecordID == req.Model.RecordID {
|
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 {
|
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))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1368,6 +1429,44 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
|||||||
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||||
return err
|
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 {
|
if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"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/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
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 {
|
if modelsChanged {
|
||||||
err = agentAccountRepo.SaveLocal(account, rows)
|
err = agentAccountRepo.SaveWithModels(account, rows)
|
||||||
} else if accountChanged {
|
} else if accountChanged {
|
||||||
err = agentAccountRepo.Save(account)
|
err = agentAccountRepo.Save(account)
|
||||||
} else {
|
} else {
|
||||||
@@ -136,6 +142,62 @@ func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBase
|
|||||||
return err
|
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) {
|
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||||
addressType := install.ModelAccountBaseURLType
|
addressType := install.ModelAccountBaseURLType
|
||||||
if addressType == "" {
|
if addressType == "" {
|
||||||
@@ -200,3 +262,39 @@ func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, cont
|
|||||||
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
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 {
|
if account == nil {
|
||||||
return nil, fmt.Errorf("account is required")
|
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")
|
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
||||||
}
|
}
|
||||||
if len(requested) > 0 {
|
if len(requested) > 0 {
|
||||||
@@ -1517,6 +1517,14 @@ func sameProviderModelID(provider, left, right string) bool {
|
|||||||
}
|
}
|
||||||
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
|
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
|
||||||
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
|
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
|
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)
|
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, info.ID)
|
||||||
if err != nil {
|
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)
|
global.LOG.Errorf("new task for exec shell failed, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -126,19 +127,17 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
|||||||
}, nil)
|
}, nil)
|
||||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||||
itemSize, err := loadModelSize(info.Name, containerName)
|
itemSize, err := loadModelSize(info.Name, containerName)
|
||||||
if len(itemSize) != 0 {
|
if err != nil {
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
return err
|
||||||
} else {
|
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
|
||||||
}
|
}
|
||||||
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)
|
}, nil)
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = 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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -167,6 +166,9 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
|||||||
if modelInfo.ID == 0 {
|
if modelInfo.ID == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
}
|
}
|
||||||
|
if modelInfo.Status == constant.StatusWaiting {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
containerName, err := LoadContainerName()
|
containerName, err := LoadContainerName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, modelInfo.ID)
|
||||||
if err != nil {
|
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)
|
global.LOG.Errorf("new task for exec shell failed, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -185,19 +188,17 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
|||||||
}, nil)
|
}, nil)
|
||||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||||
itemSize, err := loadModelSize(modelInfo.Name, containerName)
|
itemSize, err := loadModelSize(modelInfo.Name, containerName)
|
||||||
if len(itemSize) != 0 {
|
if err != nil {
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
|
return err
|
||||||
} else {
|
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
|
||||||
}
|
}
|
||||||
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)
|
}, nil)
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = 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
|
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)
|
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)
|
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||||
_ = os.Remove(logItem)
|
_ = os.Remove(logItem)
|
||||||
}
|
}
|
||||||
return SyncOllamaModelAccount(false)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
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)
|
_ = 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() {
|
go func() {
|
||||||
if taskErr := installTask.Execute(); taskErr != nil {
|
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) {
|
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
|
||||||
var calls atomic.Int32
|
var calls atomic.Int32
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"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 {
|
if req.Update {
|
||||||
total = int64(len(installDTOs))
|
total = int64(len(installDTOs))
|
||||||
}
|
}
|
||||||
@@ -168,6 +170,7 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
|||||||
res.Version = appInstall.Version
|
res.Version = appInstall.Version
|
||||||
res.CreatedAt = appInstall.CreatedAt
|
res.CreatedAt = appInstall.CreatedAt
|
||||||
res.Status = appInstall.Status
|
res.Status = appInstall.Status
|
||||||
|
res.TaskID = appInstall.TaskID
|
||||||
res.AppInstallID = appInstall.ID
|
res.AppInstallID = appInstall.ID
|
||||||
res.IsExist = true
|
res.IsExist = true
|
||||||
res.InstallPath = path.Join(global.Dir.AppInstallDir, appInstall.App.Key, appInstall.Name)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
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))
|
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -269,8 +287,8 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
return buserr.New("ErrInstallDirNotFound")
|
return buserr.New("ErrInstallDirNotFound")
|
||||||
}
|
}
|
||||||
dockerComposePath := install.GetComposePath()
|
dockerComposePath := install.GetComposePath()
|
||||||
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
if (req.UseLifecycleScripts || install.App.Key == "ollama" || install.App.Key == "vllm") && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
||||||
return operateAppWithLifecycleScripts(install, req, nil)
|
return operateAppTask(install, req, nil)
|
||||||
}
|
}
|
||||||
switch req.Operate {
|
switch req.Operate {
|
||||||
case constant.Rebuild:
|
case constant.Rebuild:
|
||||||
@@ -303,10 +321,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
UseLifecycleScripts: req.UseLifecycleScripts,
|
UseLifecycleScripts: req.UseLifecycleScripts,
|
||||||
}
|
}
|
||||||
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
return deleteAppInstall(deleteReq)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case constant.Sync:
|
case constant.Sync:
|
||||||
return syncAppInstallStatus(&install, true)
|
return syncAppInstallStatus(&install, true)
|
||||||
case constant.Upgrade:
|
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 {
|
func operateAppTask(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
|
||||||
taskType := task.TaskUpdate
|
var taskType string
|
||||||
switch req.Operate {
|
switch req.Operate {
|
||||||
case constant.Start:
|
case constant.Start:
|
||||||
|
taskType = "Run"
|
||||||
install.Status = constant.StatusStarting
|
install.Status = constant.StatusStarting
|
||||||
case constant.Restart:
|
case constant.Restart:
|
||||||
taskType = task.TaskRestart
|
taskType = task.TaskRestart
|
||||||
install.Status = constant.StatusRestarting
|
install.Status = constant.StatusRestarting
|
||||||
case constant.Stop:
|
case constant.Stop:
|
||||||
|
taskType = "Stop"
|
||||||
install.Status = constant.StatusWaiting
|
install.Status = constant.StatusWaiting
|
||||||
default:
|
default:
|
||||||
return errors.New("lifecycle script operation not supported")
|
return errors.New("lifecycle script operation not supported")
|
||||||
}
|
}
|
||||||
install.Message = ""
|
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)
|
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -358,20 +378,32 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ready := make(chan error, 1)
|
|
||||||
started := make(chan struct{})
|
|
||||||
operation := string(req.Operate)
|
operation := string(req.Operate)
|
||||||
operationTask.AddSubTaskWithOps(
|
operationTask.AddSubTaskWithOps(
|
||||||
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
||||||
func(t *task.Task) error {
|
func(t *task.Task) error {
|
||||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
if req.UseLifecycleScripts {
|
||||||
return err
|
|
||||||
}
|
|
||||||
close(started)
|
|
||||||
ready <- nil
|
|
||||||
if err := runScript(t, &install, operation); err != nil {
|
if err := runScript(t, &install, operation); err != nil {
|
||||||
return err
|
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 {
|
if req.Operate == constant.Stop {
|
||||||
install.Status = constant.StatusStopped
|
install.Status = constant.StatusStopped
|
||||||
install.Message = ""
|
install.Message = ""
|
||||||
@@ -391,14 +423,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
0,
|
0,
|
||||||
time.Hour,
|
0,
|
||||||
)
|
)
|
||||||
go func() {
|
go func() {
|
||||||
if taskErr := operationTask.Execute(); taskErr != nil {
|
taskErr := operationTask.Execute()
|
||||||
select {
|
if taskErr == nil {
|
||||||
case <-started:
|
|
||||||
default:
|
|
||||||
ready <- taskErr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if onFailure != nil {
|
if onFailure != nil {
|
||||||
@@ -408,9 +437,8 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
install.Status = constant.StatusUpErr
|
install.Status = constant.StatusUpErr
|
||||||
install.Message = taskErr.Error()
|
install.Message = taskErr.Error()
|
||||||
_ = appInstallRepo.Save(context.Background(), &install)
|
_ = appInstallRepo.Save(context.Background(), &install)
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
return <-ready
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||||
@@ -439,6 +467,19 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
oldInstalled := installed
|
||||||
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
|
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
|
||||||
if ok {
|
if ok {
|
||||||
@@ -578,7 +619,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
_ = appInstallRepo.Save(context.Background(), &failed)
|
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||||
}
|
}
|
||||||
if req.UseLifecycleScripts {
|
if req.UseLifecycleScripts {
|
||||||
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
|
err = operateAppTask(installed, request.AppInstalledOperate{
|
||||||
InstallId: installed.ID,
|
InstallId: installed.ID,
|
||||||
Operate: constant.Restart,
|
Operate: constant.Restart,
|
||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
@@ -591,10 +632,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
restoreConfig(err)
|
restoreConfig(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !req.UseLifecycleScripts {
|
|
||||||
installed.Status = constant.StatusRunning
|
|
||||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
||||||
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
||||||
@@ -1062,8 +1099,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
rawCompose := install.DockerCompose
|
rawCompose := install.DockerCompose
|
||||||
if install.App.Key != vllmAppKeyForUpgrade {
|
if install.App.Key != vllmAppKeyForUpgrade {
|
||||||
if err := ensureAppDetailCompose(install.App, &detail); err != nil {
|
if err := ensureAppDetailCompose(install.App, &detail); err != nil {
|
||||||
|
var businessErr buserr.BusinessError
|
||||||
|
if !errors.As(err, &businessErr) || businessErr.Msg != "ErrAppVersionUnavailable" {
|
||||||
return nil, err
|
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)
|
rawCompose, err = getUpgradeCompose(install, detail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1079,6 +1126,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
|
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, appInstall.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appInstall.TaskID = operationTask.ID
|
||||||
|
if operationTask.ID != "" {
|
||||||
|
appInstall.Status = localEngineTaskStatus(operationTask.Operate, appInstall.Status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
force = true
|
||||||
|
} else {
|
||||||
operation := ""
|
operation := ""
|
||||||
switch appInstall.Status {
|
switch appInstall.Status {
|
||||||
case constant.StatusInstalling:
|
case constant.StatusInstalling:
|
||||||
@@ -1089,8 +1148,10 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
|||||||
operation = task.TaskBuild
|
operation = task.TaskBuild
|
||||||
case constant.StatusUninstalling:
|
case constant.StatusUninstalling:
|
||||||
operation = task.TaskUninstall
|
operation = task.TaskUninstall
|
||||||
case constant.StatusStarting, constant.StatusWaiting:
|
case constant.StatusStarting:
|
||||||
operation = task.TaskUpdate
|
operation = "Run"
|
||||||
|
case constant.StatusWaiting:
|
||||||
|
operation = "Stop"
|
||||||
case constant.StatusRestarting:
|
case constant.StatusRestarting:
|
||||||
operation = task.TaskRestart
|
operation = task.TaskRestart
|
||||||
}
|
}
|
||||||
@@ -1116,6 +1177,7 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -74,3 +75,29 @@ func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string,
|
|||||||
}
|
}
|
||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if install.Status == constant.StatusUpgrading {
|
if install.Status == constant.StatusUpgrading && install.App.Key != "ollama" && install.App.Key != "vllm" {
|
||||||
return buserr.New("TaskIsExecuting")
|
return buserr.New("TaskIsExecuting")
|
||||||
}
|
}
|
||||||
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||||
return buserr.New("TaskIsExecuting")
|
return buserr.New("TaskIsExecuting")
|
||||||
}
|
}
|
||||||
@@ -107,6 +104,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
appUpgradeLocks.Delete(install.ID)
|
appUpgradeLocks.Delete(install.ID)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
feishuLockPath := ""
|
feishuLockPath := ""
|
||||||
if install.App.Key == constant.AppOpenclaw {
|
if install.App.Key == constant.AppOpenclaw {
|
||||||
@@ -185,7 +185,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
if ctx.rollbackErr != nil {
|
if ctx.rollbackErr != nil {
|
||||||
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
|
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
|
||||||
upgradeTask.Task.ErrorMsg = taskErr.Error()
|
upgradeTask.Task.ErrorMsg = taskErr.Error()
|
||||||
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
|
_ = taskRepo.Update(context.Background(), upgradeTask.Task)
|
||||||
}
|
}
|
||||||
if !ctx.stopAttempted || ctx.rollbackErr == nil {
|
if !ctx.stopAttempted || ctx.rollbackErr == nil {
|
||||||
restored := ctx.original
|
restored := ctx.original
|
||||||
|
|||||||
@@ -498,7 +498,11 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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() {
|
go func() {
|
||||||
if err := uninstallTask.Execute(); err != nil {
|
if err := uninstallTask.Execute(); err != nil {
|
||||||
install.Status = constant.StatusError
|
install.Status = constant.StatusError
|
||||||
@@ -1226,28 +1230,36 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecyc
|
|||||||
|
|
||||||
func rebuildApp(appInstall model.AppInstall) error {
|
func rebuildApp(appInstall model.AppInstall) error {
|
||||||
appInstall.Status = constant.StatusRebuilding
|
appInstall.Status = constant.StatusRebuilding
|
||||||
_ = appInstallRepo.Save(context.Background(), &appInstall)
|
if err := appInstallRepo.Save(context.Background(), &appInstall); err != nil {
|
||||||
go func() {
|
return err
|
||||||
dockerComposePath := appInstall.GetComposePath()
|
}
|
||||||
out, err := compose.Down(dockerComposePath)
|
buildTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskBuild, task.TaskScopeApp, "", appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handleErr(appInstall, err, out)
|
return handleErr(appInstall, err, "")
|
||||||
return
|
}
|
||||||
|
buildTask.AddSubTaskWithOps(buildTask.Name, func(t *task.Task) error {
|
||||||
|
out, err := compose.Down(appInstall.GetComposePath())
|
||||||
|
t.Log(out)
|
||||||
|
if err != nil {
|
||||||
|
return handleErr(appInstall, err, out)
|
||||||
}
|
}
|
||||||
out, err = compose.Up(appInstall.GetComposePath())
|
out, err = compose.Up(appInstall.GetComposePath())
|
||||||
|
t.Log(out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handleErr(appInstall, err, out)
|
return handleErr(appInstall, err, out)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
containerNames, err := getContainerNames(appInstall)
|
containerNames, err := getContainerNames(appInstall)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handleErr(appInstall, err, out)
|
return handleErr(appInstall, err, out)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
appInstall.ContainerName = strings.Join(containerNames, ",")
|
appInstall.ContainerName = strings.Join(containerNames, ",")
|
||||||
|
|
||||||
appInstall.Status = constant.StatusRunning
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1477,7 +1489,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
|||||||
}
|
}
|
||||||
appInstall.Status = constant.StatusError
|
appInstall.Status = constant.StatusError
|
||||||
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
|
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
|
||||||
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
|
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||||
|
} else {
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notFoundNames := make([]string, 0)
|
notFoundNames := make([]string, 0)
|
||||||
@@ -1536,30 +1552,55 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
|||||||
appInstall.Message = msg
|
appInstall.Message = msg
|
||||||
appInstall.Status = constant.StatusUnHealthy
|
appInstall.Status = constant.StatusUnHealthy
|
||||||
}
|
}
|
||||||
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
|
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||||
|
} else {
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) {
|
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) {
|
||||||
var (
|
var (
|
||||||
res []response.AppInstallDTO
|
res []response.AppInstallDTO
|
||||||
containersMap map[string]container.Summary
|
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()
|
cli, err := docker.NewClient()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
containers, err := cli.ListAllContainers()
|
containers, err := cli.ListAllContainers()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
containersMap = make(map[string]container.Summary, len(containers))
|
containersMap = make(map[string]container.Summary, len(containers))
|
||||||
for _, contain := range containers {
|
for _, contain := range containers {
|
||||||
containersMap[contain.Names[0]] = contain
|
containersMap[contain.Names[0]] = contain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, installed := range appInstallList {
|
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 {
|
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1568,7 +1609,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if sync && !doNotNeedSync(installed) {
|
if sync && !localEngine && !doNotNeedSync(installed) {
|
||||||
synAppInstall(containersMap, &installed, false)
|
synAppInstall(containersMap, &installed, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1582,6 +1623,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
AppDetailID: installed.AppDetailId,
|
AppDetailID: installed.AppDetailId,
|
||||||
Version: installed.Version,
|
Version: installed.Version,
|
||||||
Status: installed.Status,
|
Status: installed.Status,
|
||||||
|
TaskID: installed.TaskID,
|
||||||
Message: installed.Message,
|
Message: installed.Message,
|
||||||
HttpPort: installed.HttpPort,
|
HttpPort: installed.HttpPort,
|
||||||
HttpsPort: installed.HttpsPort,
|
HttpsPort: installed.HttpsPort,
|
||||||
|
|||||||
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
|
|||||||
backData = records[start:end]
|
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++ {
|
for i := 0; i < len(backData); i++ {
|
||||||
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
|
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
|
||||||
if install.ID > 0 {
|
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)
|
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||||
return 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 {
|
if cleanupErr != nil {
|
||||||
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
|
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)
|
return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
containerUpgradePullTimeout = 30 * time.Minute
|
containerUpgradePullTimeout = 30 * time.Minute
|
||||||
containerUpgradeReplaceTimeout = 15 * time.Minute
|
containerUpgradeReplaceTimeout = 15 * time.Minute
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
|||||||
if showCount == 0 {
|
if showCount == 0 {
|
||||||
return buserr.New("ErrMinQuickJump")
|
return buserr.New("ErrMinQuickJump")
|
||||||
}
|
}
|
||||||
if showCount > 4 {
|
if showCount > 5 {
|
||||||
return buserr.New("ErrMaxQuickJump")
|
return buserr.New("ErrMaxQuickJump")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +709,12 @@ func loadQuickJump(base *dto.DashboardBase) {
|
|||||||
quicks := launcherRepo.ListQuickJump(false)
|
quicks := launcherRepo.ListQuickJump(false)
|
||||||
for i := 0; i < len(quicks); i++ {
|
for i := 0; i < len(quicks); i++ {
|
||||||
switch quicks[i].Name {
|
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":
|
case "File":
|
||||||
count, err := launcherRepo.CountQuickDirectories()
|
count, err := launcherRepo.CountQuickDirectories()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package service
|
|||||||
import "github.com/1Panel-dev/1Panel/agent/app/repo"
|
import "github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
vmRepo = repo.NewIVMRepo()
|
||||||
|
modelDownloadTaskRepo = repo.NewIModelDownloadTaskRepo()
|
||||||
|
featuredModelRepo = repo.NewIFeaturedModelRepo()
|
||||||
appRepo = repo.NewIAppRepo()
|
appRepo = repo.NewIAppRepo()
|
||||||
appTagRepo = repo.NewIAppTagRepo()
|
appTagRepo = repo.NewIAppTagRepo()
|
||||||
appDetailRepo = repo.NewIAppDetailRepo()
|
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/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"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/build"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
@@ -35,6 +37,7 @@ import (
|
|||||||
type ImageService struct{}
|
type ImageService struct{}
|
||||||
|
|
||||||
type IImageService interface {
|
type IImageService interface {
|
||||||
|
SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error)
|
||||||
Page(req dto.PageImage) (int64, interface{}, error)
|
Page(req dto.PageImage) (int64, interface{}, error)
|
||||||
List() ([]dto.Options, error)
|
List() ([]dto.Options, error)
|
||||||
ListAll() ([]dto.ImageInfo, error)
|
ListAll() ([]dto.ImageInfo, error)
|
||||||
@@ -560,3 +563,17 @@ func checkUsed(imageID string, containers []container.Summary) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"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/dto/response"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"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/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"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"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -52,6 +55,7 @@ func init() {
|
|||||||
|
|
||||||
type MCPWebsiteBindingService struct {
|
type MCPWebsiteBindingService struct {
|
||||||
createWebsite func(request.WebsiteCreate) error
|
createWebsite func(request.WebsiteCreate) error
|
||||||
|
deleteWebsite func(request.WebsiteDelete) error
|
||||||
updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error
|
updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error
|
||||||
applyInclude func(model.Website) error
|
applyInclude func(model.Website) error
|
||||||
removeInclude func(model.Website) error
|
removeInclude func(model.Website) error
|
||||||
@@ -63,6 +67,7 @@ type MCPWebsiteBindingService struct {
|
|||||||
func NewMCPWebsiteBindingService() *MCPWebsiteBindingService {
|
func NewMCPWebsiteBindingService() *MCPWebsiteBindingService {
|
||||||
s := &MCPWebsiteBindingService{
|
s := &MCPWebsiteBindingService{
|
||||||
createWebsite: func(req request.WebsiteCreate) error { return NewIWebsiteService().CreateWebsite(req) },
|
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 {
|
updateHTTPS: func(ctx context.Context, req request.WebsiteHTTPSOp) error {
|
||||||
if req.Enable {
|
if req.Enable {
|
||||||
current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID)
|
current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID)
|
||||||
@@ -148,6 +153,14 @@ type mcpWebsiteOwner struct {
|
|||||||
RequestID string `json:"requestId"`
|
RequestID string `json:"requestId"`
|
||||||
RequestHash string `json:"requestHash"`
|
RequestHash string `json:"requestHash"`
|
||||||
Operation string `json:"operation"`
|
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 {
|
func mcpWebsiteHash(value string) string {
|
||||||
@@ -236,6 +249,9 @@ func mcpWebsiteFailure(reason string, definitive, conflict bool) response.MCPWeb
|
|||||||
if conflict {
|
if conflict {
|
||||||
code, message = "MCP_WEBSITE_CONFLICT", "MCP website configuration changed"
|
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}
|
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 {
|
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 {
|
func mcpWebsiteBindHash(req request.MCPWebsiteBinding) string {
|
||||||
return mcpWebsiteHash(fmt.Sprintf("%s\n%t\n%d", req.Domain, req.EnableSSL, req.SSLID))
|
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 {
|
func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBinding, site model.Website) bool {
|
||||||
host, port, err := mcpWebsiteDomain(req.Domain)
|
host, port, err := mcpWebsiteDomain(req.Domain)
|
||||||
if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID {
|
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
|
return false
|
||||||
}
|
}
|
||||||
port = installed.HttpPort
|
port = installed.HttpPort
|
||||||
if req.EnableSSL {
|
|
||||||
port = installed.HttpsPort
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, domain := range site.Domains {
|
for _, domain := range site.Domains {
|
||||||
if domain.Domain == host && domain.Port == port {
|
if domain.Domain == host && domain.Port == port && !domain.SSL {
|
||||||
return true
|
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" {
|
if req.Operation != "status" && req.Operation != "bind" && req.Operation != "update" && req.Operation != "unbind" && req.Operation != "sync" {
|
||||||
return mcpWebsiteFailure("invalid_operation", true, false)
|
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" {
|
if req.BindingID == "" && req.WebsiteID == 0 && req.Operation == "status" {
|
||||||
return response.MCPWebsiteBinding{State: "unbound"}
|
return response.MCPWebsiteBinding{State: "unbound"}
|
||||||
}
|
}
|
||||||
@@ -639,6 +832,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
|||||||
site, err := s.load(req)
|
site, err := s.load(req)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
if req.Operation == "status" || req.Operation == "unbind" {
|
if req.Operation == "status" || req.Operation == "unbind" {
|
||||||
|
if req.DeleteWebsite {
|
||||||
|
return s.missingDeletedWebsite(req)
|
||||||
|
}
|
||||||
if req.WebsiteID == 0 || req.Operation == "unbind" {
|
if req.WebsiteID == 0 || req.Operation == "unbind" {
|
||||||
return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID}
|
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)
|
domain, port, _ := mcpWebsiteDomain(req.Domain)
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = installed.HttpPort
|
port = installed.HttpPort
|
||||||
|
}
|
||||||
|
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 {
|
if req.EnableSSL {
|
||||||
port = installed.HttpsPort
|
candidate.Protocol = constant.ProtocolHTTPS
|
||||||
}
|
}
|
||||||
}
|
if conflictErr := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); conflictErr != nil {
|
||||||
if existing, conflictErr := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain), websiteDomainRepo.WithPort(port)); conflictErr == nil && existing.ID > 0 {
|
var business buserr.BusinessError
|
||||||
|
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||||
} else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) {
|
}
|
||||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||||
}
|
}
|
||||||
create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID,
|
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
|
mutationStarted = true
|
||||||
if createErr := s.createWebsite(create); createErr != nil {
|
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)
|
return mcpWebsiteFailure("website_create_failed", false, false)
|
||||||
}
|
}
|
||||||
site, err = s.load(req)
|
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)
|
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||||
}
|
}
|
||||||
if ownerErr != nil {
|
if ownerErr != nil {
|
||||||
|
if req.DeleteWebsite {
|
||||||
|
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||||
|
}
|
||||||
if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) {
|
if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) {
|
||||||
return mcpWebsiteFailure("website_ownership_changed", true, true)
|
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) {
|
} else if owner.BindingID != req.BindingID || owner.WebsiteID != site.ID || owner.KeyHash != mcpWebsiteHash(req.ProxyKey) {
|
||||||
return mcpWebsiteFailure("website_ownership_changed", true, true)
|
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" {
|
if req.Operation == "status" {
|
||||||
return s.status(req, site, owner)
|
return s.status(req, site, owner)
|
||||||
}
|
}
|
||||||
operationHash := mcpWebsitePayloadHash(req)
|
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.RequestID == req.RequestID && req.RequestID != "" {
|
||||||
if owner.RequestHash != operationHash {
|
if owner.RequestHash != operationHash {
|
||||||
return mcpWebsiteFailure("operation_request_changed", true, true)
|
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)
|
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)
|
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
|
owner.Operation, owner.RequestID, owner.RequestHash = "unbind", req.RequestID, operationHash
|
||||||
mutationStarted = true
|
mutationStarted = true
|
||||||
if err = mcpWebsiteWriteOwner(site, owner); err != nil {
|
if err = mcpWebsiteWriteOwner(site, owner); err != nil {
|
||||||
return mcpWebsiteFailure("ownership_write_failed", false, false)
|
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)
|
return s.status(req, site, owner)
|
||||||
}
|
}
|
||||||
if req.Operation == "update" {
|
if req.Operation == "update" {
|
||||||
if req.Domain != site.PrimaryDomain {
|
if req.Domain != site.PrimaryDomain {
|
||||||
return mcpWebsiteFailure("domain_changed", true, true)
|
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 {
|
if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID {
|
||||||
mutationStarted = true
|
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 {
|
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
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"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/1Panel-dev/1Panel/agent/utils/re"
|
||||||
"github.com/compose-spec/compose-go/v2/interpolation"
|
"github.com/compose-spec/compose-go/v2/interpolation"
|
||||||
"github.com/compose-spec/compose-go/v2/template"
|
"github.com/compose-spec/compose-go/v2/template"
|
||||||
@@ -16,77 +18,73 @@ import (
|
|||||||
|
|
||||||
func LoadModelGPUDevices() dto.ModelGPUDevices {
|
func LoadModelGPUDevices() dto.ModelGPUDevices {
|
||||||
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
|
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
|
||||||
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern)
|
ok, client := accelerator.New()
|
||||||
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern)
|
if !ok {
|
||||||
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern)
|
return result
|
||||||
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} {
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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)
|
entries, err := os.ReadDir(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
result.Warnings = append(result.Warnings, fmt.Sprintf("resolve Intel GPU %d render device: %v", monitored.Index, err))
|
||||||
result.Warnings = append(result.Warnings, err.Error())
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
devicePath = ""
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
var device dto.ModelGPUDevice
|
if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
|
||||||
var devicePath string
|
|
||||||
switch directory {
|
|
||||||
case "/proc/driver/nvidia/gpus":
|
|
||||||
informationPath := filepath.Join(directory, entry.Name(), "information")
|
|
||||||
content, err := os.ReadFile(informationPath)
|
|
||||||
if err != nil {
|
|
||||||
result.Warnings = append(result.Warnings, err.Error())
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
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())
|
devicePath = filepath.Join("/dev/dri", entry.Name())
|
||||||
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"}
|
break
|
||||||
case "/dev":
|
}
|
||||||
if !ascendPattern.MatchString(entry.Name()) {
|
}
|
||||||
|
if devicePath == "" {
|
||||||
|
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
|
||||||
continue
|
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)
|
info, err := os.Stat(devicePath)
|
||||||
|
device.Available = err == nil && info.Mode()&os.ModeCharDevice != 0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Warnings = append(result.Warnings, err.Error())
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,9 +647,17 @@ func resolveModelGPUValue(value string, params map[string]interface{}) (string,
|
|||||||
|
|
||||||
func loadModelAscendDeviceIDs() []string {
|
func loadModelAscendDeviceIDs() []string {
|
||||||
ids := []string{}
|
ids := []string{}
|
||||||
for _, device := range LoadModelGPUDevices().Devices {
|
entries, err := os.ReadDir("/dev")
|
||||||
if device.Type == "ascend" && device.Available {
|
if err != nil {
|
||||||
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:"))
|
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 {
|
slices.SortFunc(ids, func(a, b string) int {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/subosito/gotenv"
|
"github.com/subosito/gotenv"
|
||||||
"gopkg.in/yaml.v3"
|
"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/dto/response"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"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/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
|
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
@@ -25,13 +27,15 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var tensorRTLLMMu sync.Mutex
|
||||||
|
|
||||||
type TensorRTLLMService struct{}
|
type TensorRTLLMService struct{}
|
||||||
|
|
||||||
type ITensorRTLLMService interface {
|
type ITensorRTLLMService interface {
|
||||||
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes
|
Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error)
|
||||||
Create(create request.TensorRTLLMCreate) error
|
Create(create request.TensorRTLLMCreate) error
|
||||||
Update(req request.TensorRTLLMUpdate) error
|
Update(req request.TensorRTLLMUpdate) error
|
||||||
Delete(id uint) error
|
Delete(req request.TensorRTLLMDelete) error
|
||||||
Operate(req request.TensorRTLLMOperate) error
|
Operate(req request.TensorRTLLMOperate) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,17 +43,29 @@ func NewITensorRTLLMService() ITensorRTLLMService {
|
|||||||
return &TensorRTLLMService{}
|
return &TensorRTLLMService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes {
|
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error) {
|
||||||
var (
|
var (
|
||||||
res response.TensorRTLLMsRes
|
res response.TensorRTLLMsRes
|
||||||
items []response.TensorRTLLMDTO
|
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 {
|
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{
|
serverDTO := response.TensorRTLLMDTO{
|
||||||
TensorRTLLM: item,
|
TensorRTLLM: item,
|
||||||
|
TaskID: operationTask.ID,
|
||||||
}
|
}
|
||||||
envs, _ := gotenv.Unmarshal(item.Env)
|
envs, _ := gotenv.Unmarshal(item.Env)
|
||||||
serverDTO.Version = envs["VERSION"]
|
serverDTO.Version = envs["VERSION"]
|
||||||
@@ -105,7 +121,7 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
|
|||||||
}
|
}
|
||||||
res.Total = total
|
res.Total = total
|
||||||
res.Items = items
|
res.Items = items
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) error {
|
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 {
|
func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
||||||
|
tensorRTLLMMu.Lock()
|
||||||
|
defer tensorRTLLMMu.Unlock()
|
||||||
servers, _ := tensorrtLLMRepo.List()
|
servers, _ := tensorrtLLMRepo.List()
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
if server.ContainerName == create.ContainerName {
|
if server.ContainerName == create.ContainerName {
|
||||||
@@ -236,11 +254,6 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
|||||||
if !filesOp.Stat(tensorrtLLMDir) {
|
if !filesOp.Stat(tensorrtLLMDir) {
|
||||||
_ = filesOp.CreateDir(tensorrtLLMDir, 0644)
|
_ = filesOp.CreateDir(tensorrtLLMDir, 0644)
|
||||||
}
|
}
|
||||||
if create.ModelSpeedup {
|
|
||||||
if err := handleModelArchive(create.ModelType, create.ModelDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tensorrtLLM := &model.TensorRTLLM{
|
tensorrtLLM := &model.TensorRTLLM{
|
||||||
Name: create.Name,
|
Name: create.Name,
|
||||||
ContainerName: create.ContainerName,
|
ContainerName: create.ContainerName,
|
||||||
@@ -267,15 +280,23 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
|||||||
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
|
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go startTensorRTLLM(tensorrtLLM)
|
return runTensorRTLLMTask(tensorrtLLM, task.TaskCreate, "create", create.TaskID)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
||||||
|
tensorRTLLMMu.Lock()
|
||||||
|
defer tensorRTLLMMu.Unlock()
|
||||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 tensorrtLLM.ContainerName != req.ContainerName {
|
||||||
if err := checkContainerName(req.ContainerName); err != nil {
|
if err := checkContainerName(req.ContainerName); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -305,59 +326,118 @@ func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
|||||||
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
|
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go startTensorRTLLM(tensorrtLLM)
|
return runTensorRTLLMTask(tensorrtLLM, task.TaskUpdate, "update", req.TaskID)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TensorRTLLMService) Delete(id uint) error {
|
func (t TensorRTLLMService) Delete(req request.TensorRTLLMDelete) error {
|
||||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(id))
|
tensorRTLLMMu.Lock()
|
||||||
if err != nil {
|
defer tensorRTLLMMu.Unlock()
|
||||||
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 {
|
|
||||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
return runTensorRTLLMTask(tensorrtLLM, task.TaskDelete, "delete", req.TaskID)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTensorRTLLM(tensorrtLLM *model.TensorRTLLM) {
|
func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
|
||||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
tensorRTLLMMu.Lock()
|
||||||
if tensorrtLLM.Status != constant.StatusNormal {
|
defer tensorRTLLMMu.Unlock()
|
||||||
_, _ = compose.Down(composePath)
|
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if out, err := compose.Up(composePath); err != nil {
|
var operation string
|
||||||
tensorrtLLM.Status = constant.StatusError
|
switch req.Operate {
|
||||||
tensorrtLLM.Message = out
|
case "start":
|
||||||
|
operation = "Run"
|
||||||
|
case "stop":
|
||||||
|
operation = "Stop"
|
||||||
|
case "restart":
|
||||||
|
operation = task.TaskRestart
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported TensorRT operation: %s", req.Operate)
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
tensorrtLLM.Status = constant.StatusRunning
|
llm.Status = localEngineTaskStatus(operation, constant.StatusStarting)
|
||||||
tensorrtLLM.Message = ""
|
|
||||||
}
|
}
|
||||||
_ = syncTensorRTLLMContainerStatus(tensorrtLLM)
|
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 {
|
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
|
||||||
@@ -372,28 +452,24 @@ func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
if tensorrtLLM.Status == constant.StatusStarting {
|
tensorrtLLM.Status = constant.StatusError
|
||||||
|
tensorrtLLM.Message = buserr.WithName("ErrContainerNotFound", tensorrtLLM.ContainerName).Error()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
|
||||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
|
||||||
}
|
|
||||||
container := containers[0]
|
container := containers[0]
|
||||||
switch container.State {
|
switch container.State {
|
||||||
case "exited":
|
case "exited":
|
||||||
tensorrtLLM.Status = constant.StatusError
|
tensorrtLLM.Status = constant.StatusStopped
|
||||||
case "running":
|
case "running":
|
||||||
tensorrtLLM.Status = constant.StatusRunning
|
tensorrtLLM.Status = constant.StatusRunning
|
||||||
case "paused":
|
case "paused":
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
tensorrtLLM.Status = constant.StatusPaused
|
||||||
case "restarting":
|
case "restarting":
|
||||||
tensorrtLLM.Status = constant.StatusRestarting
|
tensorrtLLM.Status = constant.StatusRestarting
|
||||||
default:
|
default:
|
||||||
if tensorrtLLM.Status != constant.StatusStarting {
|
tensorrtLLM.Status = constant.StatusUnHealthy
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findModelArchive(modelType string) (string, error) {
|
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) {
|
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
|
||||||
|
websiteListenMutation.Lock()
|
||||||
|
defer websiteListenMutation.Unlock()
|
||||||
alias := create.Alias
|
alias := create.Alias
|
||||||
if alias == "default" {
|
if alias == "default" {
|
||||||
return buserr.New("ErrDefaultAlias")
|
return buserr.New("ErrDefaultAlias")
|
||||||
@@ -377,6 +379,11 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
|||||||
website.HttpConfig = sslReq.HttpConfig
|
website.HttpConfig = sslReq.HttpConfig
|
||||||
initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq}
|
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)
|
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) {
|
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))
|
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1012,6 +1021,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH
|
|||||||
res response.WebsiteHTTPS
|
res response.WebsiteHTTPS
|
||||||
websiteSSL model.WebsiteSSL
|
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 {
|
if err = ChangeHSTSConfig(req.Hsts, req.HstsIncludeSubDomains, req.Http3, website); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
|
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
|
||||||
|
websiteListenMutation.Lock()
|
||||||
|
defer websiteListenMutation.Unlock()
|
||||||
var (
|
var (
|
||||||
domainModels []model.WebsiteDomain
|
domainModels []model.WebsiteDomain
|
||||||
addPorts []int
|
addPorts []int
|
||||||
@@ -31,6 +33,15 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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() {
|
go func() {
|
||||||
if err := ensureFirewallPorts(addPorts); err != nil {
|
if err := ensureFirewallPorts(addPorts); err != nil {
|
||||||
global.LOG.Errorf("allow website firewall ports failed: %v", err)
|
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 (
|
const (
|
||||||
TaskScopeWebsite = "Website"
|
TaskScopeWebsite = "Website"
|
||||||
TaskScopeAI = "AI"
|
TaskScopeAI = "AI"
|
||||||
|
TaskScopeTensorRT = "TensorRT"
|
||||||
TaskScopeApp = "App"
|
TaskScopeApp = "App"
|
||||||
TaskScopeRuntime = "Runtime"
|
TaskScopeRuntime = "Runtime"
|
||||||
TaskScopeDatabase = "Database"
|
TaskScopeDatabase = "Database"
|
||||||
|
|||||||
+130
-121
@@ -8,6 +8,107 @@
|
|||||||
"formatZH": "更新 Ollama 绑定域名 [domain]",
|
"formatZH": "更新 Ollama 绑定域名 [domain]",
|
||||||
"formatEN": "update Ollama bind domain [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": {
|
"/ai/ollama/close": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
@@ -103,27 +204,27 @@
|
|||||||
"/alert/config/del": {
|
"/alert/config/del": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
|
"beforeFunctions": [],
|
||||||
"formatZH": "删除告警配置 [name]",
|
"formatZH": "删除告警配置 [name]",
|
||||||
"formatEN": "delete alert config [name]",
|
"formatEN": "delete alert config [name]"
|
||||||
"beforeFunctions": []
|
|
||||||
},
|
},
|
||||||
"/alert/config/status": {
|
"/alert/config/status": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新告警配置状态 [name][status]",
|
"formatZH": "更新告警配置状态 [name][status]",
|
||||||
"formatEN": "update alert config status [name][status]",
|
"formatEN": "update alert config status [name][status]"
|
||||||
"beforeFunctions": []
|
|
||||||
},
|
},
|
||||||
"/alert/config/update": {
|
"/alert/config/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"displayName"
|
"displayName"
|
||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新告警配置 [displayName]",
|
"formatZH": "更新告警配置 [displayName]",
|
||||||
"formatEN": "update alert config [displayName]",
|
"formatEN": "update alert config [displayName]"
|
||||||
"beforeFunctions": []
|
|
||||||
},
|
},
|
||||||
"/alert/del": {
|
"/alert/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4971,98 +5072,6 @@
|
|||||||
"formatZH": "更新离线告警日志 [id]",
|
"formatZH": "更新离线告警日志 [id]",
|
||||||
"formatEN": "update offline alert log [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": {
|
"/xpack/monitor/config/global": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
@@ -5211,7 +5220,7 @@
|
|||||||
"formatZH": "更新 vLLM [name]",
|
"formatZH": "更新 vLLM [name]",
|
||||||
"formatEN": "update vLLM [name]"
|
"formatEN": "update vLLM [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms": {
|
"/vms": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
@@ -5220,7 +5229,7 @@
|
|||||||
"formatZH": "创建虚拟机 [name]",
|
"formatZH": "创建虚拟机 [name]",
|
||||||
"formatEN": "create VM [name]"
|
"formatEN": "create VM [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/del": {
|
"/vms/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5238,7 +5247,7 @@
|
|||||||
"formatZH": "删除虚拟机 [name]",
|
"formatZH": "删除虚拟机 [name]",
|
||||||
"formatEN": "delete VM [name]"
|
"formatEN": "delete VM [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/disks/resize": {
|
"/vms/disks/resize": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"target",
|
"target",
|
||||||
@@ -5249,14 +5258,14 @@
|
|||||||
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
|
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
|
||||||
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
|
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
|
||||||
},
|
},
|
||||||
"/xpack/vms/environment/enable": {
|
"/vms/environment/enable": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "启用虚拟机运行环境",
|
"formatZH": "启用虚拟机运行环境",
|
||||||
"formatEN": "enable VM runtime environment"
|
"formatEN": "enable VM runtime environment"
|
||||||
},
|
},
|
||||||
"/xpack/vms/iso": {
|
"/vms/iso": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"type"
|
"type"
|
||||||
@@ -5266,7 +5275,7 @@
|
|||||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||||
"formatEN": "create VM ISO [name] [type]"
|
"formatEN": "create VM ISO [name] [type]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/iso/del": {
|
"/vms/iso/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5284,7 +5293,7 @@
|
|||||||
"formatZH": "删除虚拟机镜像 [name]",
|
"formatZH": "删除虚拟机镜像 [name]",
|
||||||
"formatEN": "delete VM ISO [name]"
|
"formatEN": "delete VM ISO [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/iso/update": {
|
"/vms/iso/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"type"
|
"type"
|
||||||
@@ -5294,7 +5303,7 @@
|
|||||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||||
"formatEN": "update VM ISO [name] [type]"
|
"formatEN": "update VM ISO [name] [type]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/networks": {
|
"/vms/networks": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"type"
|
"type"
|
||||||
@@ -5304,7 +5313,7 @@
|
|||||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||||
"formatEN": "create VM network [name] [type]"
|
"formatEN": "create VM network [name] [type]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/networks/del": {
|
"/vms/networks/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5322,7 +5331,7 @@
|
|||||||
"formatZH": "删除虚拟机网络 [name]",
|
"formatZH": "删除虚拟机网络 [name]",
|
||||||
"formatEN": "delete VM network [name]"
|
"formatEN": "delete VM network [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/networks/update": {
|
"/vms/networks/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5340,7 +5349,7 @@
|
|||||||
"formatZH": "更新虚拟机网络 [name]",
|
"formatZH": "更新虚拟机网络 [name]",
|
||||||
"formatEN": "update VM network [name]"
|
"formatEN": "update VM network [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/operate": {
|
"/vms/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"operate"
|
"operate"
|
||||||
@@ -5350,14 +5359,14 @@
|
|||||||
"formatZH": "操作虚拟机 [name] [operate]",
|
"formatZH": "操作虚拟机 [name] [operate]",
|
||||||
"formatEN": "operate VM [name] [operate]"
|
"formatEN": "operate VM [name] [operate]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/orphans/clean": {
|
"/vms/orphans/clean": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "清理虚拟机孤立数据",
|
"formatZH": "清理虚拟机孤立数据",
|
||||||
"formatEN": "clean VM orphaned data"
|
"formatEN": "clean VM orphaned data"
|
||||||
},
|
},
|
||||||
"/xpack/vms/rename": {
|
"/vms/rename": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"newName"
|
"newName"
|
||||||
@@ -5367,7 +5376,7 @@
|
|||||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||||
"formatEN": "rename VM [name] to [newName]"
|
"formatEN": "rename VM [name] to [newName]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/snapshots": {
|
"/vms/snapshots": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"snapshotName"
|
"snapshotName"
|
||||||
@@ -5377,7 +5386,7 @@
|
|||||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/snapshots/del": {
|
"/vms/snapshots/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"snapshotName"
|
"snapshotName"
|
||||||
@@ -5387,7 +5396,7 @@
|
|||||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/snapshots/recover": {
|
"/vms/snapshots/recover": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"snapshotName"
|
"snapshotName"
|
||||||
@@ -5397,7 +5406,7 @@
|
|||||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/storages": {
|
"/vms/storages": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"type"
|
"type"
|
||||||
@@ -5407,7 +5416,7 @@
|
|||||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||||
"formatEN": "create VM storage [name] [type]"
|
"formatEN": "create VM storage [name] [type]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/storages/del": {
|
"/vms/storages/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5425,7 +5434,7 @@
|
|||||||
"formatZH": "删除虚拟机存储 [name]",
|
"formatZH": "删除虚拟机存储 [name]",
|
||||||
"formatEN": "delete VM storage [name]"
|
"formatEN": "delete VM storage [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/storages/update": {
|
"/vms/storages/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5443,14 +5452,14 @@
|
|||||||
"formatZH": "更新虚拟机存储 [name]",
|
"formatZH": "更新虚拟机存储 [name]",
|
||||||
"formatEN": "update VM storage [name]"
|
"formatEN": "update VM storage [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/sync": {
|
"/vms/sync": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "同步虚拟机",
|
"formatZH": "同步虚拟机",
|
||||||
"formatEN": "sync virtual machines"
|
"formatEN": "sync virtual machines"
|
||||||
},
|
},
|
||||||
"/xpack/vms/templates": {
|
"/vms/templates": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name",
|
"name",
|
||||||
"templateName"
|
"templateName"
|
||||||
@@ -5460,7 +5469,7 @@
|
|||||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||||
"formatEN": "create VM [name] template [templateName]"
|
"formatEN": "create VM [name] template [templateName]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/templates/del": {
|
"/vms/templates/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5478,7 +5487,7 @@
|
|||||||
"formatZH": "删除虚拟机模板 [name]",
|
"formatZH": "删除虚拟机模板 [name]",
|
||||||
"formatEN": "delete VM template [name]"
|
"formatEN": "delete VM template [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/update": {
|
"/vms/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
|
|||||||
+11
-1
@@ -26,6 +26,12 @@ func Run() {
|
|||||||
interval model.Setting
|
interval model.Setting
|
||||||
status 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()
|
go syncBeforeStart()
|
||||||
if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil {
|
if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil {
|
||||||
global.LOG.Errorf("load monitor status from db failed, err: %v", err)
|
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)
|
global.LOG.Errorf("generate random hour failed: %v", err)
|
||||||
hourRand = big.NewInt(0)
|
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())
|
global.LOG.Errorf("can not add appstore corn job: %s", err.Error())
|
||||||
}
|
}
|
||||||
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
|
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
|
go 1.27.3
|
||||||
|
|
||||||
|
godebug multipathtcp=0
|
||||||
|
|
||||||
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
|
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
+16
-3
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Requested resource not found: {{ .err }}'
|
|||||||
|
|
||||||
# quick jump
|
# quick jump
|
||||||
ErrMinQuickJump: 'Set at least one quick jump entry'
|
ErrMinQuickJump: 'Set at least one quick jump entry'
|
||||||
ErrMaxQuickJump: 'At most 4 quick jump entries'
|
ErrMaxQuickJump: 'At most 5 quick jump entries'
|
||||||
|
|
||||||
# common
|
# common
|
||||||
ErrShutDown: 'Command stopped manually'
|
ErrShutDown: 'Command stopped manually'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Unsupported agent provider'
|
|||||||
ErrAgentAccountRequired: 'Select an agent account first'
|
ErrAgentAccountRequired: 'Select an agent account first'
|
||||||
ErrAgentAccountNotVerified: 'Agent account not verified'
|
ErrAgentAccountNotVerified: 'Agent account not verified'
|
||||||
ErrAgentAccountModelsRequired: 'Please configure at least one model'
|
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'
|
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'
|
ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it'
|
||||||
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
|
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'The certificate file does not exist'
|
|||||||
ErrSSLKeyFormat: 'Private key file verification failed'
|
ErrSSLKeyFormat: 'Private key file verification failed'
|
||||||
ErrSSLCertificateFormat: 'Invalid certificate format; use PEM'
|
ErrSSLCertificateFormat: 'Invalid certificate format; use PEM'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank'
|
||||||
|
ErrOpenrestyInstall: "Please install OpenResty from the App Store first"
|
||||||
ErrOpenrestyNotFound: 'HTTP mode requires OpenResty'
|
ErrOpenrestyNotFound: 'HTTP mode requires OpenResty'
|
||||||
ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]'
|
ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]'
|
||||||
dnsAccount: 'DNS Auto'
|
dnsAccount: 'DNS Auto'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "The SSH public and private keys do not match"
|
|||||||
# virtual machine
|
# virtual machine
|
||||||
ErrVMNotFound: "Virtual machine does not exist"
|
ErrVMNotFound: "Virtual machine does not exist"
|
||||||
ErrVMAlreadyExists: "Virtual machine already exists"
|
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 }}"
|
ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
|
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
|
||||||
ErrVMHelperNotFound: "1panel-kvm not found"
|
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."
|
AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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
|
# salto rápido
|
||||||
ErrMinQuickJump: 'Agrega al menos una entrada rápida'
|
ErrMinQuickJump: 'Agrega al menos una entrada rápida'
|
||||||
ErrMaxQuickJump: 'Máximo 4 entradas rápidas'
|
ErrMaxQuickJump: 'Máximo 5 entradas rápidas'
|
||||||
|
|
||||||
# común
|
# común
|
||||||
ErrShutDown: 'Comando detenido manualmente'
|
ErrShutDown: 'Comando detenido manualmente'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
|
|||||||
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
|
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
|
||||||
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
|
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
|
||||||
ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
|
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'
|
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'
|
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'
|
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'
|
ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada'
|
||||||
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
|
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
|
||||||
|
ErrOpenrestyInstall: "Instale primero OpenResty desde la tienda de aplicaciones"
|
||||||
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
|
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
|
||||||
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
|
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
|
||||||
dnsAccount: 'DNS Automático'
|
dnsAccount: 'DNS Automático'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
|
|||||||
# máquina virtual
|
# máquina virtual
|
||||||
ErrVMNotFound: "La máquina virtual no existe"
|
ErrVMNotFound: "La máquina virtual no existe"
|
||||||
ErrVMAlreadyExists: "La máquina virtual ya 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 }}"
|
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
|
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
|
||||||
ErrVMHelperNotFound: "No se encontró 1panel-kvm"
|
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."
|
AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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: 'حداقل یک ورودی پرش سریع تنظیم کنید'
|
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
|
||||||
ErrMaxQuickJump: 'حداکثر ۴ ورودی پرش سریع'
|
ErrMaxQuickJump: 'حداکثر ۵ ورودی پرش سریع'
|
||||||
|
|
||||||
# عمومی
|
# عمومی
|
||||||
ErrShutDown: 'دستور به صورت دستی متوقف شد'
|
ErrShutDown: 'دستور به صورت دستی متوقف شد'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'ارائهدهنده عامل پشتیبانی
|
|||||||
ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید'
|
ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید'
|
||||||
ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است'
|
ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است'
|
||||||
ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید'
|
ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید'
|
||||||
ErrAgentAccountSingleInitialModel: 'فقط یک مدل اولیه میتواند در هنگام ایجاد حساب پیکربندی شود'
|
ErrAgentAccountSingleInitialModel: 'برای این ارائهدهنده حداکثر یک مدل میتوان پیکربندی کرد'
|
||||||
ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است'
|
ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است'
|
||||||
ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده میشود. پیش از حذف، مدل بررسی دیگری انتخاب کنید'
|
ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده میشود. پیش از حذف، مدل بررسی دیگری انتخاب کنید'
|
||||||
ErrAgentFallbackModelDuplicate: 'مدلهای جایگزین نمیتوانند تکراری باشند'
|
ErrAgentFallbackModelDuplicate: 'مدلهای جایگزین نمیتوانند تکراری باشند'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'فایل گواهی وجود ندارد'
|
|||||||
ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود'
|
ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود'
|
||||||
ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید'
|
ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمیتوانند خالی باشند'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمیتوانند خالی باشند'
|
||||||
|
ErrOpenrestyInstall: "لطفاً ابتدا OpenResty را از فروشگاه برنامهها نصب کنید"
|
||||||
ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد'
|
ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد'
|
||||||
ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]'
|
ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]'
|
||||||
dnsAccount: 'خودکار DNS'
|
dnsAccount: 'خودکار DNS'
|
||||||
@@ -736,7 +737,7 @@ ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندار
|
|||||||
# ماشین مجازی
|
# ماشین مجازی
|
||||||
ErrVMNotFound: "ماشین مجازی وجود ندارد"
|
ErrVMNotFound: "ماشین مجازی وجود ندارد"
|
||||||
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
|
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
|
||||||
ErrVMCountLimit: "در نسخه Professional میتوانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
|
ErrVMCountLimit: "در نسخه فعلی میتوانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه بالاتر ارتقا دهید و دوباره تلاش کنید."
|
||||||
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
|
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "مسیر استخر ذخیرهسازی قبلاً توسط [{{ .name }}] استفاده شده است"
|
ErrVMStoragePathInUse: "مسیر استخر ذخیرهسازی قبلاً توسط [{{ .name }}] استفاده شده است"
|
||||||
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
|
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
|
||||||
@@ -803,3 +804,15 @@ ErrFeishuUpgradeMounts: "این اتصالهای ذخیرهسازی کام
|
|||||||
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
|
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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件以上追加'
|
ErrMinQuickJump: 'クイックアクセスを1件以上追加'
|
||||||
ErrMaxQuickJump: '最大4件のクイックアクセス'
|
ErrMaxQuickJump: '最大5件のクイックアクセス'
|
||||||
|
|
||||||
# 共通
|
# 共通
|
||||||
ErrShutDown: 'コマンドは手動で停止されました'
|
ErrShutDown: 'コマンドは手動で停止されました'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
|||||||
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
||||||
ErrAgentAccountNotVerified: 'アカウント未検証です'
|
ErrAgentAccountNotVerified: 'アカウント未検証です'
|
||||||
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
|
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
|
||||||
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
|
ErrAgentAccountSingleInitialModel: 'このプロバイダーで設定できるモデルは最大 1 つです'
|
||||||
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
||||||
ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください'
|
ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください'
|
||||||
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
|
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '証明書ファイルが存在しません'
|
|||||||
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
|
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
|
||||||
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
|
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
|
||||||
|
ErrOpenrestyInstall: "先にアプリストアから OpenResty をインストールしてください"
|
||||||
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
|
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
|
||||||
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
|
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
|
||||||
dnsAccount: 'DNS 自動'
|
dnsAccount: 'DNS 自動'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
|
|||||||
# 仮想マシン
|
# 仮想マシン
|
||||||
ErrVMNotFound: "仮想マシンが存在しません"
|
ErrVMNotFound: "仮想マシンが存在しません"
|
||||||
ErrVMAlreadyExists: "仮想マシンは既に存在します"
|
ErrVMAlreadyExists: "仮想マシンは既に存在します"
|
||||||
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
|
ErrVMCountLimit: "現在のエディションでは最大 {{ .limit }} 台の仮想マシンを作成できます。 既存の仮想マシンを削除するか、上位エディションにアップグレードしてから再試行してください。"
|
||||||
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
|
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
|
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
|
||||||
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
|
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
|
||||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・
|
|||||||
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
|
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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: '퀵 액세스를 하나 이상 추가하세요'
|
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
|
||||||
ErrMaxQuickJump: '퀵 액세스는 최대 4개'
|
ErrMaxQuickJump: '퀵 액세스는 최대 5개'
|
||||||
|
|
||||||
# 공통
|
# 공통
|
||||||
ErrShutDown: '명령이 수동으로 중단되었습니다'
|
ErrShutDown: '명령이 수동으로 중단되었습니다'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
|||||||
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
||||||
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
||||||
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
|
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
|
||||||
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
|
ErrAgentAccountSingleInitialModel: '이 제공업체에는 모델을 최대 하나만 설정할 수 있습니다'
|
||||||
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
||||||
ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요'
|
ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요'
|
||||||
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
|
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '인증서 파일이 존재하지 않습니다'
|
|||||||
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
|
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
|
||||||
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
|
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
|
||||||
|
ErrOpenrestyInstall: "먼저 앱 스토어에서 OpenResty를 설치하세요"
|
||||||
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
|
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
|
||||||
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
|
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
|
||||||
dnsAccount: 'DNS 자동'
|
dnsAccount: 'DNS 자동'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
|
|||||||
# 가상 머신
|
# 가상 머신
|
||||||
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
|
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
|
||||||
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
|
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
|
||||||
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
|
ErrVMCountLimit: "현재 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 상위 버전으로 업그레이드한 후 다시 시도하세요."
|
||||||
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
|
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
|
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
|
||||||
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
|
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
|
||||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원
|
|||||||
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
|
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
|
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
|
||||||
|
|
||||||
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
|
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
|
||||||
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 4 ລາຍການ'
|
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 5 ລາຍການ'
|
||||||
|
|
||||||
#common
|
#common
|
||||||
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
|
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
|
||||||
@@ -62,7 +62,7 @@ ErrAgentProviderNotSupported: 'ບໍ່ຮອງຮັບຜູ້ໃຫ້ບ
|
|||||||
ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ'
|
ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ'
|
||||||
ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ'
|
ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ'
|
||||||
ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ'
|
ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ'
|
||||||
ErrAgentAccountSingleInitialModel: 'ສາມາດກຳນົດຄ່າໂມເດລເລີ່ມຕົ້ນໄດ້ພຽງຕົວດຽວໃນລະຫວ່າງການສ້າງບັນຊີ'
|
ErrAgentAccountSingleInitialModel: 'ຜູ້ໃຫ້ບໍລິການນີ້ສາມາດກຳນົດຄ່າໄດ້ສູງສຸດໜຶ່ງໂມເດວ'
|
||||||
ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ'
|
ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ'
|
||||||
ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.'
|
ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.'
|
||||||
ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້'
|
ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້'
|
||||||
@@ -242,6 +242,7 @@ ErrSSLCertificateNotFound: 'ບໍ່ມີໄຟລ໌ໃບຮັບຮອງ
|
|||||||
ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ'
|
ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ'
|
||||||
ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM'
|
ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້'
|
||||||
|
ErrOpenrestyInstall: "ກະລຸນາຕິດຕັ້ງ OpenResty ຈາກຮ້ານແອັບກ່ອນ"
|
||||||
ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty'
|
ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty'
|
||||||
ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]'
|
ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]'
|
||||||
dnsAccount: 'DNS ອັດຕະໂນມັດ'
|
dnsAccount: 'DNS ອັດຕະໂນມັດ'
|
||||||
@@ -726,7 +727,7 @@ ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນ
|
|||||||
# virtual machine
|
# virtual machine
|
||||||
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
|
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
|
||||||
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
|
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
|
||||||
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
|
ErrVMCountLimit: "ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດລຸ້ນ ແລ້ວລອງອີກຄັ້ງ."
|
||||||
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
|
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
|
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
|
||||||
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
|
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
|
||||||
@@ -793,3 +794,15 @@ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້
|
|||||||
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
|
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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
|
# lompat pantas
|
||||||
ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas'
|
ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas'
|
||||||
ErrMaxQuickJump: 'Maksimum 4 pintasan pantas'
|
ErrMaxQuickJump: 'Maksimum 5 pintasan pantas'
|
||||||
|
|
||||||
# umum
|
# umum
|
||||||
ErrShutDown: 'Arahan diberhentikan manual'
|
ErrShutDown: 'Arahan diberhentikan manual'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
|
|||||||
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
|
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
|
||||||
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
|
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
|
||||||
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
|
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'
|
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
|
||||||
ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya'
|
ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya'
|
||||||
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
|
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Fail sijil tidak wujud'
|
|||||||
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
|
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
|
||||||
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
|
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
|
||||||
|
ErrOpenrestyInstall: "Sila pasang OpenResty daripada gedung aplikasi terlebih dahulu"
|
||||||
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
|
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
|
||||||
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
|
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
|
||||||
dnsAccount: 'DNS automatik'
|
dnsAccount: 'DNS automatik'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
|
|||||||
# mesin maya
|
# mesin maya
|
||||||
ErrVMNotFound: "Mesin maya tidak wujud"
|
ErrVMNotFound: "Mesin maya tidak wujud"
|
||||||
ErrVMAlreadyExists: "Mesin maya sudah 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 }}"
|
ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
|
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
|
||||||
ErrVMHelperNotFound: "1panel-kvm tidak ditemui"
|
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."
|
AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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
|
# atalho rápido
|
||||||
ErrMinQuickJump: 'Adicione pelo menos um 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
|
# comum
|
||||||
ErrShutDown: 'Comando parado manualmente'
|
ErrShutDown: 'Comando parado manualmente'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
|
|||||||
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
|
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
|
||||||
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
|
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
|
||||||
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
|
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'
|
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'
|
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'
|
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'
|
ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada'
|
||||||
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
|
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ou EabHmacKey não podem estar em branco'
|
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'
|
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 }}]'
|
ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]'
|
||||||
dnsAccount: 'DNS Automático'
|
dnsAccount: 'DNS Automático'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
|
|||||||
# máquina virtual
|
# máquina virtual
|
||||||
ErrVMNotFound: "Máquina virtual não existe"
|
ErrVMNotFound: "Máquina virtual não existe"
|
||||||
ErrVMAlreadyExists: "Máquina virtual já 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 }}"
|
ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
|
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
|
||||||
ErrVMHelperNotFound: "1panel-kvm não encontrado"
|
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."
|
AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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: 'Добавьте хотя бы одну быструю ссылку'
|
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
|
||||||
ErrMaxQuickJump: 'Не более 4 быстрых ссылок'
|
ErrMaxQuickJump: 'Не более 5 быстрых ссылок'
|
||||||
|
|
||||||
# общее
|
# общее
|
||||||
ErrShutDown: 'Команда остановлена вручную'
|
ErrShutDown: 'Команда остановлена вручную'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер
|
|||||||
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
||||||
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
||||||
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
|
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
|
||||||
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
|
ErrAgentAccountSingleInitialModel: 'Для этого провайдера можно настроить не более одной модели'
|
||||||
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
||||||
ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки'
|
ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки'
|
||||||
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
|
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Файл сертификата не существ
|
|||||||
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
|
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
|
||||||
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
|
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
|
||||||
|
ErrOpenrestyInstall: "Сначала установите OpenResty из магазина приложений"
|
||||||
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
|
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
|
||||||
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
|
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
|
||||||
dnsAccount: 'DNS Авто'
|
dnsAccount: 'DNS Авто'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не с
|
|||||||
# виртуальная машина
|
# виртуальная машина
|
||||||
ErrVMNotFound: "Виртуальная машина не существует"
|
ErrVMNotFound: "Виртуальная машина не существует"
|
||||||
ErrVMAlreadyExists: "Виртуальная машина уже существует"
|
ErrVMAlreadyExists: "Виртуальная машина уже существует"
|
||||||
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку."
|
ErrVMCountLimit: "В текущей редакции можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на старшую редакцию и повторите попытку."
|
||||||
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
|
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
|
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
|
||||||
ErrVMHelperNotFound: "1panel-kvm не найден"
|
ErrVMHelperNotFound: "1panel-kvm не найден"
|
||||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Эти монтирования не поддержив
|
|||||||
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
|
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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ş
|
# hızlı geçiş
|
||||||
ErrMinQuickJump: 'En az bir hızlı erişim ekle'
|
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
|
# genel
|
||||||
ErrShutDown: 'Komut manuel durduruldu'
|
ErrShutDown: 'Komut manuel durduruldu'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
|
|||||||
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
|
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
|
||||||
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
|
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
|
||||||
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
|
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ı'
|
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'
|
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'
|
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'
|
ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız'
|
||||||
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
|
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
|
||||||
|
ErrOpenrestyInstall: "Lütfen önce uygulama mağazasından OpenResty yükleyin"
|
||||||
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
|
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
|
||||||
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
|
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
|
||||||
dnsAccount: 'DNS Otomatik'
|
dnsAccount: 'DNS Otomatik'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
|
|||||||
# sanal makine
|
# sanal makine
|
||||||
ErrVMNotFound: "Sanal makine mevcut değil"
|
ErrVMNotFound: "Sanal makine mevcut değil"
|
||||||
ErrVMAlreadyExists: "Sanal makine zaten mevcut"
|
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 }}"
|
ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
|
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
|
||||||
ErrVMHelperNotFound: "1panel-kvm bulunamadı"
|
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."
|
AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu."
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
|
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: '請至少設定一個快速跳轉入口!'
|
ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
|
||||||
ErrMaxQuickJump: '最多可設定四個快速跳轉入口!'
|
ErrMaxQuickJump: '最多可設定五個快速跳轉入口!'
|
||||||
|
|
||||||
# 通用
|
# 通用
|
||||||
ErrShutDown: '指令被手動結束!'
|
ErrShutDown: '指令被手動結束!'
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
|
|||||||
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
|
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
|
||||||
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
|
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
|
||||||
ErrAgentAccountModelsRequired: '請至少配置一個模型'
|
ErrAgentAccountModelsRequired: '請至少配置一個模型'
|
||||||
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
|
ErrAgentAccountSingleInitialModel: '目前提供商最多可設定一個模型'
|
||||||
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
|
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
|
||||||
ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除'
|
ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除'
|
||||||
ErrAgentFallbackModelDuplicate: '備用模型不能重複'
|
ErrAgentFallbackModelDuplicate: '備用模型不能重複'
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '憑證檔案不存在'
|
|||||||
ErrSSLKeyFormat: '私鑰檔案驗證失敗'
|
ErrSSLKeyFormat: '私鑰檔案驗證失敗'
|
||||||
ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式'
|
ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空'
|
||||||
|
ErrOpenrestyInstall: "請先在應用程式商店安裝 OpenResty"
|
||||||
ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty'
|
ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty'
|
||||||
ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]'
|
ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]'
|
||||||
dnsAccount: 'DNS 自動'
|
dnsAccount: 'DNS 自動'
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
|
|||||||
# 虛擬機
|
# 虛擬機
|
||||||
ErrVMNotFound: "虛擬機不存在"
|
ErrVMNotFound: "虛擬機不存在"
|
||||||
ErrVMAlreadyExists: "虛擬機已存在"
|
ErrVMAlreadyExists: "虛擬機已存在"
|
||||||
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
|
ErrVMCountLimit: "目前版本最多可建立 {{ .limit }} 台虛擬機。 請刪除現有虛擬機或升級版本後重試。"
|
||||||
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
|
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
|
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
|
||||||
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
||||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應
|
|||||||
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
|
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "等待容器升級任務執行名額"
|
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 个快速跳转入口"
|
ErrMinQuickJump: "至少设置 1 个快速跳转入口"
|
||||||
ErrMaxQuickJump: "最多设置 4 个快速跳转入口"
|
ErrMaxQuickJump: "最多设置 5 个快速跳转入口"
|
||||||
|
|
||||||
# 通用
|
# 通用
|
||||||
ErrShutDown: "命令已手动终止"
|
ErrShutDown: "命令已手动终止"
|
||||||
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商"
|
|||||||
ErrAgentAccountRequired: "请选择智能体账号后重试"
|
ErrAgentAccountRequired: "请选择智能体账号后重试"
|
||||||
ErrAgentAccountNotVerified: "账号未通过验证"
|
ErrAgentAccountNotVerified: "账号未通过验证"
|
||||||
ErrAgentAccountModelsRequired: "请至少配置一个模型"
|
ErrAgentAccountModelsRequired: "请至少配置一个模型"
|
||||||
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
|
ErrAgentAccountSingleInitialModel: '当前提供商最多配置一个模型'
|
||||||
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
|
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
|
||||||
ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除"
|
ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除"
|
||||||
ErrAgentFallbackModelDuplicate: "备用模型不能重复"
|
ErrAgentFallbackModelDuplicate: "备用模型不能重复"
|
||||||
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '证书文件不存在'
|
|||||||
ErrSSLKeyFormat: '私钥文件校验失败'
|
ErrSSLKeyFormat: '私钥文件校验失败'
|
||||||
ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
|
ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
|
||||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空'
|
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空'
|
||||||
|
ErrOpenrestyInstall: "请先在应用商店安装 OpenResty"
|
||||||
ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty'
|
ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty'
|
||||||
ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]'
|
ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]'
|
||||||
dnsAccount: "DNS 自动"
|
dnsAccount: "DNS 自动"
|
||||||
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
|
|||||||
# 虚拟机
|
# 虚拟机
|
||||||
ErrVMNotFound: "虚拟机不存在"
|
ErrVMNotFound: "虚拟机不存在"
|
||||||
ErrVMAlreadyExists: "虚拟机已存在"
|
ErrVMAlreadyExists: "虚拟机已存在"
|
||||||
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
|
ErrVMCountLimit: "当前版本最多可创建 {{ .limit }} 台虚拟机。 请删除已有虚拟机或升级版本后重试。"
|
||||||
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
|
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
|
||||||
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
|
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
|
||||||
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
ErrVMHelperNotFound: "1panel-kvm 不存在"
|
||||||
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应
|
|||||||
AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。"
|
AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。"
|
||||||
|
|
||||||
ContainerUpgradeWaiting: "等待容器升级任务执行名额"
|
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() {
|
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 syncApp()
|
||||||
go syncInstalledApp()
|
go syncInstalledApp()
|
||||||
go syncRuntime()
|
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.AddQuickDirectory,
|
||||||
migrations.AddLocalModelAccountLink,
|
migrations.AddLocalModelAccountLink,
|
||||||
migrations.AddOllamaModelAccountSync,
|
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
|
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 = gin.Default()
|
||||||
Router.Use(i18n.UseI18n())
|
Router.Use(i18n.UseI18n())
|
||||||
|
Router.Use(middleware.MCPErrorMetadata())
|
||||||
registerMCPRuntime(Router)
|
registerMCPRuntime(Router)
|
||||||
|
|
||||||
PrivateGroup := Router.Group("/api/v2")
|
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
|
return
|
||||||
}
|
}
|
||||||
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
|
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]
|
meta, ok := metaMap[reqPath]
|
||||||
if !ok || len(meta.BeforeFunctions) == 0 {
|
if !ok || len(meta.BeforeFunctions) == 0 {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
func commonGroups() []CommonRouter {
|
func commonGroups() []CommonRouter {
|
||||||
return []CommonRouter{
|
return []CommonRouter{
|
||||||
|
&VMRouter{},
|
||||||
&DashboardRouter{},
|
&DashboardRouter{},
|
||||||
&HostRouter{},
|
&HostRouter{},
|
||||||
&ContainerRouter{},
|
&ContainerRouter{},
|
||||||
@@ -23,6 +24,7 @@ func commonGroups() []CommonRouter {
|
|||||||
&ProcessRouter{},
|
&ProcessRouter{},
|
||||||
&WebsiteCARouter{},
|
&WebsiteCARouter{},
|
||||||
&AIToolsRouter{},
|
&AIToolsRouter{},
|
||||||
|
&ModelDownloaderRouter{},
|
||||||
&GroupRouter{},
|
&GroupRouter{},
|
||||||
&AlertRouter{},
|
&AlertRouter{},
|
||||||
&RuntimeDiagnosticsRouter{},
|
&RuntimeDiagnosticsRouter{},
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("", baseApi.ContainerCreate)
|
baRouter.POST("", baseApi.ContainerCreate)
|
||||||
baRouter.POST("/update", baseApi.ContainerUpdate)
|
baRouter.POST("/update", baseApi.ContainerUpdate)
|
||||||
baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
|
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("/info", baseApi.ContainerInfo)
|
||||||
baRouter.POST("/search", baseApi.SearchContainer)
|
baRouter.POST("/search", baseApi.SearchContainer)
|
||||||
baRouter.POST("/list", baseApi.ListContainer)
|
baRouter.POST("/list", baseApi.ListContainer)
|
||||||
@@ -64,6 +67,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("/template", baseApi.CreateComposeTemplate)
|
baRouter.POST("/template", baseApi.CreateComposeTemplate)
|
||||||
baRouter.POST("/template/del", baseApi.DeleteComposeTemplate)
|
baRouter.POST("/template/del", baseApi.DeleteComposeTemplate)
|
||||||
|
|
||||||
|
baRouter.POST("/image/remote/search", baseApi.SearchRemoteImages)
|
||||||
baRouter.GET("/image", baseApi.ListImage)
|
baRouter.GET("/image", baseApi.ListImage)
|
||||||
baRouter.GET("/image/all", baseApi.ListAllImage)
|
baRouter.GET("/image/all", baseApi.ListAllImage)
|
||||||
baRouter.POST("/image/search", baseApi.SearchImage)
|
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/dir"
|
||||||
"github.com/1Panel-dev/1Panel/agent/init/firewall"
|
"github.com/1Panel-dev/1Panel/agent/init/firewall"
|
||||||
"github.com/1Panel-dev/1Panel/agent/init/hook"
|
"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/lang"
|
||||||
"github.com/1Panel-dev/1Panel/agent/init/log"
|
"github.com/1Panel-dev/1Panel/agent/init/log"
|
||||||
"github.com/1Panel-dev/1Panel/agent/init/migration"
|
"github.com/1Panel-dev/1Panel/agent/init/migration"
|
||||||
@@ -122,6 +123,7 @@ func Start() {
|
|||||||
global.LOG.Info("agent startup: firewall init scheduled")
|
global.LOG.Info("agent startup: firewall init scheduled")
|
||||||
InitOthers()
|
InitOthers()
|
||||||
global.LOG.Info("agent startup: edition initialized")
|
global.LOG.Info("agent startup: edition initialized")
|
||||||
|
kvm.Init()
|
||||||
|
|
||||||
rootRouter := router.Routers()
|
rootRouter := router.Routers()
|
||||||
global.LOG.Info("agent startup: router initialized")
|
global.LOG.Info("agent startup: router initialized")
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type Device struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Basic struct {
|
type Basic struct {
|
||||||
|
DrmDevice string `json:"-"`
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
|
|
||||||
DeviceID int `json:"deviceID"`
|
DeviceID int `json:"deviceID"`
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
|||||||
xpu := Device{
|
xpu := Device{
|
||||||
ProcessStatus: "unavailable",
|
ProcessStatus: "unavailable",
|
||||||
Basic: Basic{
|
Basic: Basic{
|
||||||
|
DrmDevice: device.DrmDevice,
|
||||||
UUID: device.UUID,
|
UUID: device.UUID,
|
||||||
DeviceID: device.DeviceID,
|
DeviceID: device.DeviceID,
|
||||||
DeviceName: device.DeviceName,
|
DeviceName: device.DeviceName,
|
||||||
@@ -137,6 +138,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if info.DrmDevice != "" {
|
||||||
|
xpu.Basic.DrmDevice = info.DrmDevice
|
||||||
|
}
|
||||||
xpu.Basic.DriverVersion = info.DriverVersion
|
xpu.Basic.DriverVersion = info.DriverVersion
|
||||||
|
|
||||||
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
|
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 {
|
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)
|
out, err := c.cli.ImagePull(ctx, imageName, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package dockerhub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SearchItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResult struct {
|
||||||
|
Items []SearchItem `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
ProxyRequired bool `json:"proxyRequired"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search uses the panel HTTP client, not the Docker daemon's proxy settings.
|
||||||
|
func Search(ctx context.Context, client *http.Client, query string, page, pageSize int) (SearchResult, error) {
|
||||||
|
result := SearchResult{Items: []SearchItem{}}
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" || len([]rune(query)) > 256 || page < 1 || pageSize < 1 || pageSize > 100 {
|
||||||
|
return result, fmt.Errorf("invalid Docker Hub search parameters")
|
||||||
|
}
|
||||||
|
params := url.Values{"query": {query}, "page": {strconv.Itoa(page)}, "page_size": {strconv.Itoa(pageSize)}}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://hub.docker.com/v2/search/repositories/?"+params.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// Transport errors can contain proxy credentials; do not expose their raw text.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return result, ctx.Err()
|
||||||
|
}
|
||||||
|
return result, fmt.Errorf("Docker Hub connection failed; check network and proxy settings")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return result, fmt.Errorf("Docker Hub search: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Count *int64 `json:"count"`
|
||||||
|
Results []struct {
|
||||||
|
Name string `json:"repo_name"`
|
||||||
|
Description string `json:"short_description"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(&body); err != nil {
|
||||||
|
return result, fmt.Errorf("invalid Docker Hub search response")
|
||||||
|
}
|
||||||
|
if body.Count == nil || *body.Count < 0 {
|
||||||
|
return result, fmt.Errorf("invalid Docker Hub search response")
|
||||||
|
}
|
||||||
|
result.Total = *body.Count
|
||||||
|
for _, item := range body.Results {
|
||||||
|
if item.Name == "" {
|
||||||
|
return result, fmt.Errorf("invalid Docker Hub repository name")
|
||||||
|
}
|
||||||
|
result.Items = append(result.Items, SearchItem{Name: item.Name, Description: item.Description})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user