Compare commits

..
Author SHA1 Message Date
ssongliu ce429dbf67 fix(ai): refine model service forms and Ollama drawers 2026-11-03 00:03:35 +08:00
蘭 e29679768b feat: add MCP error metadata handling and improve source policy resolution (#14270)
* feat: add MCP error metadata handling and improve source policy resolution

* fix: adjust height-diff for improved table layout
2026-11-02 22:17:12 +08:00
CityFun 8e1b761f4c fix(app): allow upgrades from removed versions (#14280) 2026-11-02 22:17:03 +08:00
蘭 963c1463a8 fix: update MCP access permissions and improve titles in routing (#14281) 2026-11-02 22:16:53 +08:00
ssongliu 2401bb16a2 feat(ai): connect local model accounts and add Ollama management page (#14282)
* feat(ai): connect local model accounts and add Ollama management page

* fix(ai): register Ollama management frontend path
2026-11-02 22:16:43 +08:00
ssongliu c06c4e338b feat(vm): enable community virtual machines and migrate xpack data (#14279)
* feat(vm): enable community virtual machines and migrate xpack data

* fix(vm): stop ISO download polling on failure or cancellation

* fix(vm): respect deleteFile when deleting failed ISO imports
2026-11-02 18:22:58 +08:00
ssongliu f8c61993c5 feat(ai): open source model marketplace and migrate historical data (#14277)
* feat(ai): open source model marketplace and migrate historical data

* fix(ai): wait for canceled model downloads before retrying
2026-11-02 17:46:26 +08:00
ssongliu b31e683f1b fix: correct local inference engine operation task logs (#14274) 2026-11-02 17:27:43 +08:00
Yuki Guo 50f0a37bee feat: add remote Docker Hub image search (#14271) 2026-11-02 11:35:26 +08:00
ssongliu 51cf40a648 feat(ai): show local inference engine ports (#14273) 2026-11-02 11:21:17 +08:00
Yuki Guo 7ec31cd5e9 Merge pull request #14246 from 1Panel-dev/pr@dev-v2@feat_image_check
feat(container): add image update checks with scheduling and task logs
2026-11-02 10:48:03 +08:00
Yuki Guo 50a45b72f3 Merge branch 'dev-v2' into pr@dev-v2@feat_image_check 2026-11-02 09:44:27 +08:00
ssongliu 13362f84a6 fix(ai): synchronize inference engine task states and unify management (#14269) 2026-11-01 23:36:15 +08:00
ssongliu 046dc94d05 fix(script): bypass stale resource caches during library sync (#14268) 2026-11-01 22:32:58 +08:00
ssongliu 2059119d89 fix(dashboard): restore monitor layout and compact single GPU status (#14267) 2026-11-01 22:32:06 +08:00
ssongliu 7f4c503982 fix(ai): improve GPU capability hints and separate engine logs (#14263) 2026-11-01 01:06:37 +08:00
ssongliu 93ebc6a8b9 feat(ai): streamline Ollama model pool management (#14259) 2026-10-30 23:50:11 +08:00
王贺andzhengkunwang223 9a49f38ffd fix(network): disable MPTCP by default (#14258)
Set multipathtcp=0 in the core and agent module defaults so Linux listeners use standard TCP without requiring service environment changes.

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

Some files were not shown because too many files have changed in this diff Show More