mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-29 00:00:17 +00:00
Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f91db55daa | ||
|
|
5346d5f58a | ||
|
|
ac09426f10 | ||
|
|
19707a68dd | ||
|
|
7c7fd492c4 | ||
|
|
fbf66766c2 | ||
|
|
d7355f3ccc | ||
|
|
93ca724985 | ||
|
|
9481a7535d | ||
|
|
14be5b7870 | ||
|
|
c4b76a8e89 | ||
|
|
9295f4ef8a | ||
|
|
c925184bf6 | ||
|
|
87828b03ae | ||
|
|
e7b4146e1b | ||
|
|
ec998552b9 | ||
|
|
1af237944d | ||
|
|
f9f9169a4d | ||
|
|
1e01761ba3 | ||
|
|
15283fde34 | ||
|
|
49f8e7f040 | ||
|
|
98b27af6a0 | ||
|
|
a393224729 | ||
|
|
b851c84290 | ||
|
|
372855157d | ||
|
|
577d771495 | ||
|
|
ca3e6511e5 | ||
|
|
8af14eae25 | ||
|
|
175fbaf3f2 | ||
|
|
13199efd62 | ||
|
|
04099ae2a2 | ||
|
|
c695a2a408 | ||
|
|
690cb48280 | ||
|
|
f226fed8cd | ||
|
|
110c292856 | ||
|
|
8e865c2c46 | ||
|
|
17b8f472e7 | ||
|
|
d9dc5d7aa9 | ||
|
|
547f9775b0 | ||
|
|
f37afb1a37 | ||
|
|
3b89d83554 | ||
|
|
12bfe6a1bd | ||
|
|
aaf70cfc41 | ||
|
|
1ecd4f7f0c | ||
|
|
a8ccdcade7 | ||
|
|
4a89c0c031 | ||
|
|
f21ebb1a36 | ||
|
|
3011383b07 | ||
|
|
fd12866897 | ||
|
|
bc6cf519ce | ||
|
|
f9c43989fe | ||
|
|
e90b201001 | ||
|
|
f7394938f7 | ||
|
|
dada7294dd | ||
|
|
afcb94f6ef | ||
|
|
5a2e430dff | ||
|
|
f2ca4f56e6 | ||
|
|
b6f609aaee | ||
|
|
c960c0137d | ||
|
|
09a901294e | ||
|
|
4d481a8f31 | ||
|
|
f1e0aa3391 | ||
|
|
f475500543 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c |
+12
-16
@@ -72,22 +72,6 @@ func (b *BaseApi) CloseOllamaModel(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
|
||||||
// @Summary Sync Ollama model list
|
|
||||||
// @Success 200 {array} dto.OllamaModelDropList
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/ollama/model/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
|
|
||||||
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
|
|
||||||
list, err := aiToolService.Sync()
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -114,6 +98,18 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
|
||||||
|
var req dto.OllamaModelAccountSync
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aiToolService.SyncAccount(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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) AIBenchmarkSources(c *gin.Context) {
|
||||||
|
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, sources)
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/update [post]
|
// @Router /alert/config/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
|
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
|
||||||
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||||
var req dto.AlertConfigUpdate
|
var req dto.AlertConfigUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
@@ -318,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/status [post]
|
// @Router /alert/config/status [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||||
var req dto.AlertConfigStatusUpdate
|
var req dto.AlertConfigStatusUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -350,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/del [post]
|
// @Router /alert/config/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
|
||||||
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
||||||
var req dto.DeleteRequest
|
var req dto.DeleteRequest
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := alertService.DeleteAlertConfig(req.ID)
|
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -378,10 +384,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Type == constant.Custom {
|
if req.Type == constant.Custom {
|
||||||
result, err := alertService.TestCustomAlertConfig(req)
|
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||||
|
if lang == "" {
|
||||||
|
lang = i18n.GetLanguageFromDB()
|
||||||
|
}
|
||||||
|
result, err := alertService.TestCustomAlertConfig(req, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
if global.LOG != nil {
|
||||||
return
|
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
|
||||||
|
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
|
||||||
|
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
result = dto.AlertConfigTestResult{Success: false}
|
||||||
|
}
|
||||||
|
result.Response = ""
|
||||||
|
result.Message = ""
|
||||||
|
if !result.Success {
|
||||||
|
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,10 +161,25 @@ func (b *BaseApi) GetAppDetailByID(c *gin.Context) {
|
|||||||
// @Router /apps/install [post]
|
// @Router /apps/install [post]
|
||||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
||||||
func (b *BaseApi) InstallApp(c *gin.Context) {
|
func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req request.AppInstallCreate
|
var req request.AppInstallCreate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
req.MCPPreserveWebsiteDir = true
|
||||||
|
if value, exists := req.Params["WEBSITE_DIR"]; exists {
|
||||||
|
directory, ok := value.(string)
|
||||||
|
if !ok || directory == "" || directory != settingService.GetWebsiteDir() {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
install, err := appService.Install(req, true)
|
install, err := appService.Install(req, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -336,11 +340,25 @@ func (b *BaseApi) UpdateAppConfig(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /apps/installed/info/:appInstallId [get]
|
// @Router /apps/installed/info/:appInstallId [get]
|
||||||
func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
appInstallId, err := helper.GetIntParamByKey(c, "appInstallId")
|
appInstallId, err := helper.GetIntParamByKey(c, "appInstallId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
info, err := (&service.AppInstallService{}).GetAppInstallInfoForMCP(c.Request.Context(), appInstallId)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, info)
|
||||||
|
return
|
||||||
|
}
|
||||||
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := containerService.Page(req)
|
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.ContainerStatus
|
// @Success 200 {object} dto.ContainerStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param containersOnly query boolean false "Only count containers"
|
||||||
// @Router /containers/status [get]
|
// @Router /containers/status [get]
|
||||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||||
data, err := containerService.LoadStatus()
|
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
|||||||
// @Success 200 {array} dto.ContainerListStats
|
// @Success 200 {array} dto.ContainerListStats
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||||
// @Router /containers/list/stats [get]
|
// @Router /containers/list/stats [get]
|
||||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||||
data, err := containerService.ContainerListStats()
|
var ids []string
|
||||||
|
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||||
|
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||||
|
}
|
||||||
|
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,11 +101,25 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
|
|||||||
// @Param type path string true "type"
|
// @Param type path string true "type"
|
||||||
// @Router /databases/db/list/:type [get]
|
// @Router /databases/db/list/:type [get]
|
||||||
func (b *BaseApi) ListDatabase(c *gin.Context) {
|
func (b *BaseApi) ListDatabase(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
dbType, err := helper.GetStrParamByKey(c, "type")
|
dbType, err := helper.GetStrParamByKey(c, "type")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
list, err := (&service.DatabaseService{}).ListForMCP(c.Request.Context(), dbType)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
return
|
||||||
|
}
|
||||||
list, err := databaseService.List(dbType)
|
list, err := databaseService.List(dbType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +22,11 @@ import (
|
|||||||
// @Router /databases [post]
|
// @Router /databases [post]
|
||||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
||||||
func (b *BaseApi) CreateMysql(c *gin.Context) {
|
func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req dto.MysqlDBCreate
|
var req dto.MysqlDBCreate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
@@ -32,6 +40,16 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
req.Password = string(password)
|
req.Password = string(password)
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
created, err := (&service.MysqlService{}).CreateForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodeID, _ := mcpapi.NodeID(c.Request)
|
||||||
|
helper.SuccessWithData(c, map[string]any{"id": created.ID, "database": created.MysqlName, "name": created.Name, "nodeId": nodeID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -513,11 +531,25 @@ func (b *BaseApi) LoadRemoteAccess(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/status [post]
|
// @Router /databases/status [post]
|
||||||
func (b *BaseApi) LoadStatus(c *gin.Context) {
|
func (b *BaseApi) LoadStatus(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req dto.OperationWithNameAndType
|
var req dto.OperationWithNameAndType
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mcpRequest {
|
||||||
|
data, err := (&service.MysqlService{}).LoadStatusForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
data, err := mysqlService.LoadStatus(req)
|
data, err := mysqlService.LoadStatus(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
// @Summary Install redis-cli
|
// @Summary Install redis-cli
|
||||||
// @Success 200
|
// @Accept json
|
||||||
|
// @Param request body dto.RedisCliInstall true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/redis/install/cli [post]
|
// @Router /databases/redis/install/cli [post]
|
||||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||||
if err := redisService.InstallCli(); err != nil {
|
var req dto.RedisCliInstall
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := redisService.InstallCli(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
|
|||||||
+80
-139
@@ -4,11 +4,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
@@ -90,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/forward/base [post]
|
// @Router /hosts/firewall/forward/base [post]
|
||||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||||
data, err := forwardingService.LoadBaseInfo()
|
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -111,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, items, err := forwardingService.SearchRules(request)
|
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -262,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, info)
|
helper.SuccessWithData(c, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Adopt an external firewall rule
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleAdopt true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/adopt [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
|
||||||
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleAdopt
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall rule creation
|
// @Summary Queue firewall rule creation
|
||||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||||
@@ -309,69 +285,9 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Preview firewall rule synchronization
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
|
||||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Load the currently executing firewall rule synchronization task
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/task [get]
|
|
||||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
|
||||||
result, err := firewallService.CurrentRuleSyncTask()
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Synchronize firewall rules to a target backend
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
|
||||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall rule deletion
|
// @Summary Queue firewall rule deletion
|
||||||
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
|
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleDelete true "request"
|
// @Param request body dto.FirewallRuleDelete true "request"
|
||||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||||
@@ -394,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Update a managed unified firewall v2 rule
|
// @Summary Update a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -402,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/update [post]
|
// @Router /hosts/firewall/rules/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleUpdate
|
var request dto.FirewallRuleUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -419,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Reorder a managed unified firewall v2 rule
|
// @Summary Reorder a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleReorder true "request"
|
// @Param request body dto.FirewallRuleReorder true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -427,35 +340,19 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/reorder [post]
|
// @Router /hosts/firewall/rules/reorder [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleReorder
|
var request dto.FirewallRuleReorder
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
|
||||||
if value == nil {
|
|
||||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
*value = strings.TrimSpace(*value)
|
|
||||||
if *value == "" {
|
|
||||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||||
var businessErr buserr.BusinessError
|
var businessErr buserr.BusinessError
|
||||||
isBusinessError := errors.As(err, &businessErr)
|
isBusinessError := errors.As(err, &businessErr)
|
||||||
@@ -464,21 +361,20 @@ func handleFirewallRuleError(c *gin.Context, err error) {
|
|||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrRuleStale):
|
case errors.Is(err, filter.ErrRuleStale):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
|
||||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
|
||||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
case errors.Is(err, filter.ErrVerificationFailed):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
|
||||||
default:
|
default:
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
@@ -501,7 +397,7 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Create firewall port whitelist rules
|
// @Summary Create firewall port whitelist rules
|
||||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -523,7 +419,7 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Update firewall port whitelist rules
|
// @Summary Update firewall port whitelist rules
|
||||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -545,7 +441,7 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Delete firewall port whitelist rules
|
// @Summary Delete firewall port whitelist rules
|
||||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -622,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, data)
|
helper.SuccessWithData(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Sync Docker port guard rules
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/docker/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
|
||||||
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
|
||||||
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
|
||||||
handleDockerPortGuardError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Operate Docker port guard
|
// @Summary Operate Docker port guard
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -675,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||||
var request dto.DockerPortGuardPolicyBatchDelete
|
var request dto.DockerPortGuardPolicyBatchDelete
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
@@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) {
|
|||||||
}
|
}
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary List firewall rule backups
|
||||||
|
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||||
|
// @Success 200 {object} dto.FirewallRuleBackups
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/backups [get]
|
||||||
|
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||||
|
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Initialize, repair or bind one firewall address family
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/family/operate [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||||
|
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||||
|
var request dto.FirewallFamilyOperation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateFamily(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Update firewall IPv6 support
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||||
|
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||||
|
var request dto.FirewallIPv6Operation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateIPv6(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package helper
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -21,6 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: "",
|
Message: "",
|
||||||
@@ -31,6 +34,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
ErrorCode: businessCode,
|
ErrorCode: businessCode,
|
||||||
@@ -41,6 +45,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -184,3 +189,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
|||||||
intNum, _ := strconv.Atoi(idParam)
|
intNum, _ := strconv.Atoi(idParam)
|
||||||
return int32(intNum), nil
|
return int32(intNum), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOperationLogValues supplies server-resolved values to Core's operation log.
|
||||||
|
// Call before writing the response. Only pass public audit fields, never credentials.
|
||||||
|
func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||||
|
const header = "X-Op-Resolved"
|
||||||
|
resolved := make(map[string]string)
|
||||||
|
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
|
||||||
|
_ = json.Unmarshal(data, &resolved)
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
resolved = make(map[string]string)
|
||||||
|
}
|
||||||
|
for key, value := range values {
|
||||||
|
resolved[key] = value
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(resolved)
|
||||||
|
if err == nil {
|
||||||
|
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-23
@@ -1,13 +1,9 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
// @Summary Clean monitor data
|
// @Summary Clean host or GPU monitor data
|
||||||
|
// @Param request body dto.MonitorClean true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/clean [post]
|
// @Router /hosts/monitor/clean [post]
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||||
if err := monitorService.CleanData(); err != nil {
|
var req dto.MonitorClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanData(req.Type); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/netoptions [get]
|
// @Router /hosts/monitor/netoptions [get]
|
||||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||||
netStat, _ := net.IOCounters(true)
|
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range netStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/iooptions [get]
|
// @Router /hosts/monitor/iooptions [get]
|
||||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||||
diskStat, _ := disk.IOCounters()
|
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range diskStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMCurrent
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary List quick directories
|
||||||
|
// @Success 200 {array} model.QuickDirectory
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [get]
|
||||||
|
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||||
|
items, err := dashboardService.ListQuickDirectories()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Create quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [post]
|
||||||
|
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.CreateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Update quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/update [post]
|
||||||
|
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Delete quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/del [post]
|
||||||
|
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -61,13 +61,14 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/del [post]
|
// @Router /runtimes/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeDelete
|
var req request.RuntimeDelete
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := runtimeService.Delete(req)
|
name, err := runtimeService.Delete(req)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -169,7 +170,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/operate [post]
|
// @Router /runtimes/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeOperate
|
var req request.RuntimeOperate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.CreateRootCert(req); err != nil {
|
if err := sshService.CreateRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.EditRootCert(req); err != nil {
|
if err := sshService.EditRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ func (b *BaseApi) ChangeDefaultServer(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /websites/php/version [post]
|
// @Router /websites/php/version [post]
|
||||||
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||||
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
||||||
var req request.WebsitePHPVersionReq
|
var req request.WebsitePHPVersionReq
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,3 +179,64 @@ func (b *BaseApi) DownloadCAFile(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Upload website CA archive
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "ZIP containing ca.crt and ca.key"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/upload [post]
|
||||||
|
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
|
||||||
|
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Request.MultipartForm != nil {
|
||||||
|
defer c.Request.MultipartForm.RemoveAll()
|
||||||
|
}
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Import website CA from PEM or server files
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteCAImport true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/import [post]
|
||||||
|
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
|
||||||
|
var req request.WebsiteCAImport
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteCAService.Import(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
|||||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||||
BridgePort int `json:"bridgePort"`
|
BridgePort int `json:"bridgePort"`
|
||||||
AllowedOrigins []string `json:"allowedOrigins"`
|
AllowedOrigins []string `json:"allowedOrigins"`
|
||||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentItem struct {
|
type AgentItem struct {
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
@@ -384,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountInfo struct {
|
type AgentAccountInfo struct {
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
MasterAccountID uint `json:"masterAccountId"`
|
MasterAccountID uint `json:"masterAccountId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
|||||||
+6
-5
@@ -14,16 +14,17 @@ type OllamaModelInfo struct {
|
|||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OllamaModelDropList struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OllamaModelName struct {
|
type OllamaModelName struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OllamaModelAccountSync struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
}
|
||||||
|
|
||||||
type OllamaBindDomain struct {
|
type OllamaBindDomain struct {
|
||||||
Domain string `json:"domain" validate:"required"`
|
Domain string `json:"domain" validate:"required"`
|
||||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type AIBenchmarkSource struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Tokenizer string `json:"tokenizer"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ type AlertBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PushAlert struct {
|
type PushAlert struct {
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
TaskName string `json:"taskName"`
|
TaskName string `json:"taskName"`
|
||||||
AlertType string `json:"alertType"`
|
AlertType string `json:"alertType"`
|
||||||
EntryID uint `json:"entryID"`
|
EntryID uint `json:"entryID"`
|
||||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
|
TaskName string `json:"taskName,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SendCount uint `json:"sendCount"`
|
SendCount uint `json:"sendCount"`
|
||||||
AdvancedParams string `json:"advancedParams"`
|
AdvancedParams string `json:"advancedParams"`
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ type ContainerItemStats struct {
|
|||||||
}
|
}
|
||||||
type ContainerListStats struct {
|
type ContainerListStats struct {
|
||||||
ContainerID string `json:"containerID"`
|
ContainerID string `json:"containerID"`
|
||||||
|
ContainerName string `json:"containerName"`
|
||||||
|
|
||||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||||
SystemUsage uint64 `json:"systemUsage"`
|
SystemUsage uint64 `json:"systemUsage"`
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type CronjobOperate struct {
|
|||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
|
|
||||||
CleanLogConfig
|
CleanLogConfig
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ type CronjobInfo struct {
|
|||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronjobImport struct {
|
type CronjobImport struct {
|
||||||
@@ -172,6 +174,7 @@ type CronjobTrans struct {
|
|||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
}
|
}
|
||||||
type TransHelper struct {
|
type TransHelper struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -31,7 +31,27 @@ type DashboardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangeQuicks struct {
|
type ChangeQuicks struct {
|
||||||
Quicks []QuickJump `json:"quicks"`
|
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickVisibility struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
IsShow bool `json:"isShow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryCreate struct {
|
||||||
|
Path string `json:"path" validate:"required"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryDelete struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryUpdate struct {
|
||||||
|
QuickDirectoryDelete
|
||||||
|
QuickDirectoryCreate
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
|||||||
Port int64 `json:"port"`
|
Port int64 `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisCliInstall struct {
|
||||||
|
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// mysql
|
// mysql
|
||||||
type MysqlDBSearch struct {
|
type MysqlDBSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
|
|||||||
+64
-99
@@ -2,11 +2,13 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FirewallSubsystemStatus struct {
|
type FirewallSubsystemStatus struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Backend string `json:"backend"`
|
Backend string `json:"backend"`
|
||||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||||
@@ -18,13 +20,13 @@ type FirewallSubsystemStatus struct {
|
|||||||
PingStatus string `json:"pingStatus"`
|
PingStatus string `json:"pingStatus"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
SyncError string `json:"syncError,omitempty"`
|
|
||||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallLifecycleOperation struct {
|
type FirewallLifecycleOperation struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
@@ -49,11 +51,11 @@ type FirewallBackendOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendFamilyStatus struct {
|
type FirewallBackendFamilyStatus struct {
|
||||||
|
Partial bool `json:"partial"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
Bound bool `json:"bound"`
|
Bound bool `json:"bound"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
|
||||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ type FirewallBackendGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSettings struct {
|
type FirewallSettings struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
System FirewallBackendGroup `json:"system"`
|
System FirewallBackendGroup `json:"system"`
|
||||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||||
Docker FirewallBackendGroup `json:"docker"`
|
Docker FirewallBackendGroup `json:"docker"`
|
||||||
@@ -92,6 +95,17 @@ type FirewallBackendOperation struct {
|
|||||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallIPv6Operation struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallFamilyOperation struct {
|
||||||
|
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||||
|
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||||
|
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilterChainOperation struct {
|
type FilterChainOperation struct {
|
||||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||||
@@ -104,6 +118,7 @@ type FilterChainOperationResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallInitializationTask struct {
|
type FirewallInitializationTask struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,23 +129,36 @@ type FirewallRuleInventoryResponse struct {
|
|||||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
AllTotal int64 `json:"allTotal"`
|
AllTotal int64 `json:"allTotal"`
|
||||||
ManagedTotal int64 `json:"managedTotal"`
|
|
||||||
Items []filter.InventoryItem `json:"items"`
|
Items []filter.InventoryItem `json:"items"`
|
||||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackup struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider filter.Provider `json:"provider"`
|
||||||
|
RuleCount int `json:"ruleCount"`
|
||||||
|
ModifiedAt int64 `json:"modifiedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackups struct {
|
||||||
|
Directory string `json:"directory"`
|
||||||
|
Files []FirewallRuleBackup `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
type FirewallRuleResetResponse struct {
|
type FirewallRuleResetResponse struct {
|
||||||
|
BackupPath string `json:"backupPath"`
|
||||||
Removed int `json:"removed"`
|
Removed int `json:"removed"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReset struct {
|
type FirewallRuleReset struct {
|
||||||
|
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||||
|
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleInventory struct {
|
type FirewallRuleInventory struct {
|
||||||
Refresh bool `json:"refresh,omitempty"`
|
|
||||||
PageInfo
|
PageInfo
|
||||||
Scope filter.Scope `json:"scope,omitempty"`
|
Scope filter.Scope `json:"scope,omitempty"`
|
||||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||||
@@ -138,7 +166,6 @@ type FirewallRuleInventory struct {
|
|||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||||
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
|
||||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +177,7 @@ type FirewallNativeDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardBase struct {
|
type DockerPortGuardBase struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
@@ -162,6 +190,7 @@ type DockerPortGuardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardFamilyStatus struct {
|
type DockerPortGuardFamilyStatus struct {
|
||||||
|
Partial bool `json:"partial"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
@@ -182,11 +211,8 @@ type DockerPortGuardEndpoint struct {
|
|||||||
Application string `json:"application,omitempty"`
|
Application string `json:"application,omitempty"`
|
||||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
NativeAction string `json:"nativeAction,omitempty"`
|
|
||||||
ReadOnly bool `json:"readOnly,omitempty"`
|
|
||||||
Sources []string `json:"sources"`
|
Sources []string `json:"sources"`
|
||||||
Effective bool `json:"effective"`
|
Effective bool `json:"effective"`
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
TrafficPath string `json:"trafficPath"`
|
TrafficPath string `json:"trafficPath"`
|
||||||
ManagementTarget string `json:"managementTarget"`
|
ManagementTarget string `json:"managementTarget"`
|
||||||
ManagementReason string `json:"managementReason,omitempty"`
|
ManagementReason string `json:"managementReason,omitempty"`
|
||||||
@@ -223,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicyBatch struct {
|
type DockerPortGuardPolicyBatch struct {
|
||||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||||
|
Import bool `json:"import"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardPolicyBatchDelete struct {
|
type DockerPortGuardPolicyBatchDelete struct {
|
||||||
@@ -231,31 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
type DockerPortGuardPolicy struct {
|
||||||
DockerPortGuardEndpointIdentity
|
DockerPortGuardEndpointIdentity
|
||||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||||
Description string `json:"description" validate:"max=256"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardOperation struct {
|
type DockerPortGuardOperation struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleAdopt struct {
|
|
||||||
Scope filter.Scope `json:"scope" validate:"required"`
|
|
||||||
InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
|
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
|
||||||
Marker string `json:"marker,omitempty" validate:"max=256"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleCreateItem struct {
|
type FirewallRuleCreateItem struct {
|
||||||
|
Raw string `json:"raw,omitempty"`
|
||||||
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||||
SourceID string `json:"sourceID"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreate struct {
|
type FirewallRuleCreate struct {
|
||||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
|
Initialize bool `json:"initialize"`
|
||||||
|
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreateResponse struct {
|
type FirewallRuleCreateResponse struct {
|
||||||
@@ -274,66 +297,13 @@ type FirewallRuleCreateFailure struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleSyncRequest struct {
|
|
||||||
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
|
||||||
ResetSource bool `json:"resetSource"`
|
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncItem struct {
|
|
||||||
SourceUUID string `json:"sourceUUID"`
|
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
|
||||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
|
||||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
|
||||||
Status firewallsync.Status `json:"status"`
|
|
||||||
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
|
||||||
Reason string `json:"reason,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncPreview struct {
|
|
||||||
Subsystem string `json:"subsystem"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Ready int `json:"ready"`
|
|
||||||
Existing int `json:"existing"`
|
|
||||||
Removed int `json:"removed"`
|
|
||||||
Blocked int `json:"blocked"`
|
|
||||||
Items []FirewallRuleSyncItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncResult struct {
|
|
||||||
Subsystem string `json:"subsystem"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Succeeded int `json:"succeeded"`
|
|
||||||
Skipped int `json:"skipped"`
|
|
||||||
Removed int `json:"removed"`
|
|
||||||
Failed int `json:"failed"`
|
|
||||||
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
|
||||||
TaskID string `json:"taskID,omitempty"`
|
|
||||||
Queued bool `json:"queued,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncTask struct {
|
|
||||||
TaskID string `json:"taskID,omitempty"`
|
|
||||||
Executing bool `json:"executing"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncFailure struct {
|
|
||||||
SourceUUID string `json:"sourceUUID"`
|
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
|
||||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
|
||||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleDelete struct {
|
type FirewallRuleDelete struct {
|
||||||
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
|
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||||
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteItem struct {
|
||||||
|
FirewallRuleDeleteTarget
|
||||||
|
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleDeleteTarget struct {
|
type FirewallRuleDeleteTarget struct {
|
||||||
@@ -351,12 +321,12 @@ type FirewallRuleDeleteResponse struct {
|
|||||||
|
|
||||||
type FirewallRuleDeleteFailure struct {
|
type FirewallRuleDeleteFailure struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
UUID string `json:"uuid"`
|
InstanceKey string `json:"instanceKey"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleUpdate struct {
|
type FirewallRuleUpdate struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||||
@@ -364,26 +334,21 @@ type FirewallRuleUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReorder struct {
|
type FirewallRuleReorder struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
TargetPosition *int64 `json:"targetPosition"`
|
TargetPosition *int64 `json:"targetPosition"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
type FirewallRuleExportItem struct {
|
||||||
p.Items = append(p.Items, item)
|
filter.FirewallRule
|
||||||
switch item.Status {
|
Raw string `json:"raw,omitempty"`
|
||||||
case firewallsync.StatusReady:
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
p.Ready++
|
}
|
||||||
p.Total++
|
|
||||||
case firewallsync.StatusExisting:
|
type FirewallSubsystemBackup struct {
|
||||||
p.Existing++
|
Families []string `json:"families,omitempty"`
|
||||||
p.Total++
|
Subsystem string `json:"subsystem"`
|
||||||
case firewallsync.StatusRemove:
|
Provider filter.Provider `json:"provider"`
|
||||||
p.Removed++
|
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||||
case firewallsync.StatusBlocked:
|
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||||
p.Blocked++
|
|
||||||
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
|
||||||
p.Total++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,10 @@ type ForwardRule struct {
|
|||||||
|
|
||||||
UsedStatus string `json:"usedStatus"`
|
UsedStatus string `json:"usedStatus"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
IsDesired bool `json:"isDesired"`
|
|
||||||
IsRuntime bool `json:"isRuntime"`
|
|
||||||
SyncStatus string `json:"syncStatus"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleOperate struct {
|
type ForwardRuleOperate struct {
|
||||||
ForceDelete bool `json:"forceDelete"`
|
Import bool `json:"import"`
|
||||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
-13
@@ -1,6 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
type MonitorSearch struct {
|
type MonitorSearch struct {
|
||||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||||
@@ -26,6 +31,12 @@ type Process struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSetting struct {
|
type MonitorSetting struct {
|
||||||
|
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||||
|
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||||
|
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||||
|
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||||
|
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||||
|
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||||
MonitorStatus string `json:"monitorStatus"`
|
MonitorStatus string `json:"monitorStatus"`
|
||||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||||
MonitorInterval string `json:"monitorInterval"`
|
MonitorInterval string `json:"monitorInterval"`
|
||||||
@@ -34,16 +45,19 @@ type MonitorSetting struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSettingUpdate struct {
|
type MonitorSettingUpdate struct {
|
||||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPUOptions struct {
|
type MonitorGPUOptions struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
GPUType string `json:"gpuType"`
|
GPUType string `json:"gpuType"`
|
||||||
ChartHide []GPUChartHide `json:"chartHide"`
|
ChartHide []GPUChartHide `json:"chartHide"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
}
|
}
|
||||||
type GPUChartHide struct {
|
type GPUChartHide struct {
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Process bool `json:"process"`
|
Process bool `json:"process"`
|
||||||
@@ -55,23 +69,54 @@ type GPUChartHide struct {
|
|||||||
Speed bool `json:"speed"`
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
type MonitorGPUSearch struct {
|
type MonitorGPUSearch struct {
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
type MonitorGPUData struct {
|
type MonitorGPUData struct {
|
||||||
Date []time.Time `json:"date"`
|
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||||
GPUValue []float64 `json:"gpuValue"`
|
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||||
TemperatureValue []float64 `json:"temperatureValue"`
|
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||||
PowerTotal []float64 `json:"powerTotal"`
|
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||||
PowerUsed []float64 `json:"powerUsed"`
|
OFAUtil []*float64 `json:"ofaUtil"`
|
||||||
PowerPercent []float64 `json:"powerPercent"`
|
MediaUtil []*float64 `json:"mediaUtil"`
|
||||||
MemoryTotal []float64 `json:"memoryTotal"`
|
ComputeUtil []*float64 `json:"computeUtil"`
|
||||||
MemoryUsed []float64 `json:"memoryUsed"`
|
CopyUtil []*float64 `json:"copyUtil"`
|
||||||
MemoryPercent []float64 `json:"memoryPercent"`
|
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||||
SpeedValue []int `json:"speedValue"`
|
FanRPM []*float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed []*float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal []*float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed []*float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal []*float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
ProcessCount []int `json:"processCount"`
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||||
|
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||||
|
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||||
|
Date []time.Time `json:"date"`
|
||||||
|
GPUValue []*float64 `json:"gpuValue"`
|
||||||
|
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||||
|
PowerTotal []*float64 `json:"powerTotal"`
|
||||||
|
PowerUsed []*float64 `json:"powerUsed"`
|
||||||
|
PowerPercent []*float64 `json:"powerPercent"`
|
||||||
|
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||||
|
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||||
|
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||||
|
SpeedValue []*float64 `json:"speedValue"`
|
||||||
|
|
||||||
|
ProcessCount []*float64 `json:"processCount"`
|
||||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,3 +126,34 @@ type GPUProcess struct {
|
|||||||
ProcessName string `json:"processName"`
|
ProcessName string `json:"processName"`
|
||||||
UsedMemory string `json:"usedMemory"`
|
UsedMemory string `json:"usedMemory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMSearch struct {
|
||||||
|
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
StartTime time.Time `json:"startTime" validate:"required"`
|
||||||
|
EndTime time.Time `json:"endTime" validate:"required"`
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMData struct {
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
Points []model.MonitorVLLM `json:"points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrent struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrentData struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMClean struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorClean struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ type AppInstallCreate struct {
|
|||||||
Services map[string]string `json:"services"`
|
Services map[string]string `json:"services"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
|
|
||||||
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
|
MCPPreserveWebsiteDir bool `json:"-"`
|
||||||
|
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
NodePushConfig
|
NodePushConfig
|
||||||
}
|
}
|
||||||
@@ -122,6 +128,9 @@ type AppInstallDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledUpdate struct {
|
type AppInstalledUpdate struct {
|
||||||
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
Params map[string]interface{} `json:"params" validate:"required"`
|
Params map[string]interface{} `json:"params" validate:"required"`
|
||||||
TaskID string `json:"-"`
|
TaskID string `json:"-"`
|
||||||
|
|||||||
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
|
|||||||
PushNode bool `json:"pushNode"`
|
PushNode bool `json:"pushNode"`
|
||||||
Nodes string `json:"nodes"`
|
Nodes string `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsiteCAImport struct {
|
||||||
|
Name string `json:"name" validate:"required,max=128"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||||
|
Certificate string `json:"certificate" validate:"required_if=Type paste"`
|
||||||
|
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
|
||||||
|
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
|
||||||
|
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ type TagDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledCheck struct {
|
type AppInstalledCheck struct {
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
App string `json:"app"`
|
App string `json:"app"`
|
||||||
@@ -184,6 +186,12 @@ type AppParam struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
AppKey string `json:"appKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HttpPort int `json:"httpPort"`
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
Params []AppParam `json:"params"`
|
Params []AppParam `json:"params"`
|
||||||
RawCompose string `json:"rawCompose"`
|
RawCompose string `json:"rawCompose"`
|
||||||
request.AppContainerConfig
|
request.AppContainerConfig
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
EncryptionMode string `json:"encryptionMode"`
|
||||||
PassPhrase string `json:"passPhrase"`
|
PassPhrase string `json:"passPhrase"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
type AgentAccount struct {
|
type AgentAccount struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ type AppInstall struct {
|
|||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||||
|
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
App App `json:"app" gorm:"-:migration"`
|
App App `json:"app" gorm:"-:migration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuickDirectory struct {
|
||||||
|
BaseModel
|
||||||
|
Path string `json:"path"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Legacy bool `json:"legacy" gorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
UUID string `gorm:"uniqueIndex" json:"uuid"`
|
|
||||||
ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
|
||||||
Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
|
||||||
HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
|
||||||
HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
|
||||||
Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Sources string `gorm:"type:text" json:"-"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
NativeAction string `gorm:"default:''" json:"-"`
|
|
||||||
NativeRules string `gorm:"type:text" json:"-"`
|
|
||||||
Sequence int64 `gorm:"default:0" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForwardingRule struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
|
||||||
Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
|
||||||
Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
|
||||||
TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
|
||||||
TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
|
||||||
Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRule struct {
|
|
||||||
UUID string `gorm:"primaryKey" json:"uuid"`
|
|
||||||
Family string `json:"family"`
|
|
||||||
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
SourceAddress string `json:"sourceAddress"`
|
|
||||||
SourcePort string `json:"sourcePort"`
|
|
||||||
DestinationAddress string `json:"destinationAddress"`
|
|
||||||
DestinationPort string `json:"destinationPort"`
|
|
||||||
Interface string `json:"interface"`
|
|
||||||
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
|
||||||
Priority *int `json:"priority,omitempty"`
|
|
||||||
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
|
||||||
|
|
||||||
Origin string `json:"origin"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Revision uint `gorm:"default:1" json:"revision"`
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type MonitorBase struct {
|
type MonitorBase struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
@@ -33,14 +35,88 @@ type MonitorNetwork struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPU struct {
|
type MonitorGPU struct {
|
||||||
|
MemoryUtil *float64 `json:"memoryUtil"`
|
||||||
|
MemoryActivity *float64 `json:"memoryActivity"`
|
||||||
|
EncoderUtil *float64 `json:"encoderUtil"`
|
||||||
|
DecoderUtil *float64 `json:"decoderUtil"`
|
||||||
|
JPEGUtil *float64 `json:"jpegUtil"`
|
||||||
|
OFAUtil *float64 `json:"ofaUtil"`
|
||||||
|
MediaUtil *float64 `json:"mediaUtil"`
|
||||||
|
ComputeUtil *float64 `json:"computeUtil"`
|
||||||
|
CopyUtil *float64 `json:"copyUtil"`
|
||||||
|
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||||
|
FanRPM *float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed *float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal *float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed *float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal *float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
|
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
DeviceType string `json:"deviceType"`
|
||||||
|
ProcessStatus string `json:"processStatus"`
|
||||||
|
Frequency *float64 `json:"frequency"`
|
||||||
|
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||||
|
IntervalSeconds int `json:"intervalSeconds"`
|
||||||
BaseModel
|
BaseModel
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
GPUUtil float64 `json:"gpuUtil"`
|
GPUUtil *float64 `json:"gpuUtil"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature *float64 `json:"temperature"`
|
||||||
PowerDraw float64 `json:"powerDraw"`
|
PowerDraw *float64 `json:"powerDraw"`
|
||||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||||
MemUsed float64 `json:"memUsed"`
|
MemUsed *float64 `json:"memUsed"`
|
||||||
MemTotal float64 `json:"memTotal"`
|
MemTotal *float64 `json:"memTotal"`
|
||||||
FanSpeed int `json:"fanSpeed"`
|
FanSpeed *float64 `json:"fanSpeed"`
|
||||||
Processes string `json:"processes"`
|
Processes string `json:"processes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLM struct {
|
||||||
|
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
AppInstallID uint `json:"appInstallID"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RawMetrics string `json:"-"`
|
||||||
|
HistogramDeltas string `json:"-"`
|
||||||
|
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||||
|
SampleIntervalSeconds int `json:"-"`
|
||||||
|
RequestDeltas string `json:"-"`
|
||||||
|
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
|
||||||
|
QueueTime *float64 `json:"queueTime"`
|
||||||
|
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
|
||||||
|
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
|
||||||
|
FinishReasons map[string]float64 `json:"finishReasons" gorm:"-"`
|
||||||
|
PromptDistribution map[string]float64 `json:"promptDistribution" gorm:"-"`
|
||||||
|
GenerationDistribution map[string]float64 `json:"generationDistribution" gorm:"-"`
|
||||||
|
|
||||||
|
Running *float64 `json:"running"`
|
||||||
|
Waiting *float64 `json:"waiting"`
|
||||||
|
CacheUsage *float64 `json:"cacheUsage"`
|
||||||
|
PromptThroughput *float64 `json:"promptThroughput"`
|
||||||
|
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||||
|
RequestThroughput *float64 `json:"requestThroughput"`
|
||||||
|
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||||
|
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||||
|
RequestLatency *float64 `json:"requestLatency"`
|
||||||
|
PrefillTime *float64 `json:"prefillTime"`
|
||||||
|
DecodeTime *float64 `json:"decodeTime"`
|
||||||
|
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||||
|
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||||
|
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||||
|
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||||
|
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||||
|
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||||
|
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||||
|
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||||
|
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||||
|
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||||
|
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||||
|
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
@@ -82,7 +82,6 @@ var catalog = map[string]Meta{
|
|||||||
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||||
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||||
{ID: "glm-5", Name: "GLM-5"},
|
{ID: "glm-5", Name: "GLM-5"},
|
||||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
|
||||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||||
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||||
},
|
},
|
||||||
@@ -114,7 +113,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
|
||||||
},
|
},
|
||||||
"xiaomi": {
|
"xiaomi": {
|
||||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxCatalog(t *testing.T) {
|
||||||
|
meta, ok := Get("minimax")
|
||||||
|
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
|
||||||
|
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
|
||||||
|
}
|
||||||
|
if meta.DefaultAPIType != "anthropic-messages" {
|
||||||
|
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
|
||||||
|
}
|
||||||
|
for _, provider := range All() {
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
|
||||||
|
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxVerificationRequests(t *testing.T) {
|
||||||
|
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
|
||||||
|
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
|
||||||
|
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
|
||||||
|
base, endpoint := region+"/v1", "/chat/completions"
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
base, endpoint = region+"/anthropic", "/v1/messages"
|
||||||
|
}
|
||||||
|
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
|
||||||
|
if built.URL != base+endpoint {
|
||||||
|
t.Fatalf("URL = %s", built.URL)
|
||||||
|
}
|
||||||
|
var captured map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
|
||||||
|
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
|
||||||
|
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
|
||||||
|
t.Error("incorrect API key authentication")
|
||||||
|
}
|
||||||
|
} else if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||||
|
t.Error("incorrect bearer authentication")
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
|
||||||
|
t.Fatalf("unexpected serialized body: %#v", captured)
|
||||||
|
}
|
||||||
|
if model == "MiniMax-M3.1-Flash" {
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
|
||||||
|
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
|
||||||
|
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
|
||||||
|
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
|
||||||
|
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
|
||||||
|
t.Fatalf("changed explicit %s request: %#v", provider, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
headers["x-api-key"] = apiKey
|
headers["x-api-key"] = apiKey
|
||||||
}
|
}
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
headers["anthropic-version"] = "2023-06-01"
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["thinking"] = map[string]string{"type": "adaptive"}
|
||||||
|
body["output_config"] = map[string]string{"effort": "max"}
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
case "openai-responses":
|
case "openai-responses":
|
||||||
request.URL = baseURL + "/responses"
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
@@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
}
|
}
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["reasoning_effort"] = "max"
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ type IAgentAccountRepo interface {
|
|||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
|
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -97,6 +99,25 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Save(account).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range models {
|
||||||
|
models[i].AccountID = account.ID
|
||||||
|
models[i].SortOrder = i + 1
|
||||||
|
}
|
||||||
|
return tx.Create(&models).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeProviders(providers []string) []string {
|
func normalizeProviders(providers []string) []string {
|
||||||
seen := make(map[string]struct{}, len(providers))
|
seen := make(map[string]struct{}, len(providers))
|
||||||
result := make([]string, 0, len(providers))
|
result := make([]string, 0, len(providers))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
)
|
)
|
||||||
@@ -14,6 +16,7 @@ type IAiRepo interface {
|
|||||||
Create(cronjob *model.OllamaModel) error
|
Create(cronjob *model.OllamaModel) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
|
DeleteAll(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAiRepo() IAiRepo {
|
func NewIAiRepo() IAiRepo {
|
||||||
@@ -67,3 +70,7 @@ func (u *AiRepo) Delete(opts ...DBOption) error {
|
|||||||
}
|
}
|
||||||
return db.Delete(&model.OllamaModel{}).Error
|
return db.Delete(&model.OllamaModel{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AiRepo) DeleteAll(ctx context.Context) error {
|
||||||
|
return getTx(ctx).Where("1 = 1").Delete(&model.OllamaModel{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LauncherRepo struct{}
|
type LauncherRepo struct{}
|
||||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
|||||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||||
ListQuickJump(withAll bool) []model.QuickJump
|
ListQuickJump(withAll bool) []model.QuickJump
|
||||||
UpdateQuicks(quicks []model.QuickJump) error
|
UpdateQuicks(quicks []model.QuickJump) error
|
||||||
|
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||||
|
CountQuickDirectories() (int64, error)
|
||||||
|
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
DeleteQuickDirectory(id uint, legacy bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewILauncherRepo() ILauncherRepo {
|
func NewILauncherRepo() ILauncherRepo {
|
||||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
|||||||
for _, item := range quicks {
|
for _, item := range quicks {
|
||||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||||
"is_show": item.IsShow,
|
"is_show": item.IsShow,
|
||||||
"detail": item.Detail,
|
|
||||||
"alias": item.Alias,
|
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tx.Commit()
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||||
|
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||||
|
items := make([]model.QuickDirectory, 0)
|
||||||
|
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var quicks []model.QuickJump
|
||||||
|
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, quick := range quicks {
|
||||||
|
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||||
|
var count, legacyCount int64
|
||||||
|
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count + legacyCount, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
return global.DB.Create(item).Error
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if item.Legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IDockerPortGuardRepo interface {
|
|
||||||
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
|
||||||
DeleteBatch(context.Context, []string) error
|
|
||||||
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerPortGuardRepo struct{}
|
|
||||||
|
|
||||||
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
|
||||||
var policies []model.DockerPortGuardPolicy
|
|
||||||
err := global.DB.WithContext(ctx).
|
|
||||||
Where("read_only = ?", false).
|
|
||||||
Order("family, host_ip, host_port, protocol").
|
|
||||||
Find(&policies).Error
|
|
||||||
return policies, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
|
||||||
return global.DB.WithContext(ctx).
|
|
||||||
Where("read_only = ? AND uuid IN ?", false, uuids).
|
|
||||||
Delete(&model.DockerPortGuardPolicy{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
for i := range policies {
|
|
||||||
policies[i].ReadOnly = false
|
|
||||||
if err := tx.Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
|
||||||
}).Create(&policies[i]).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
|
||||||
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
|
||||||
)
|
|
||||||
|
|
||||||
type IFirewallRuleRepo interface {
|
|
||||||
Create(context.Context, *model.FirewallRule) error
|
|
||||||
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
|
||||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
|
||||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
|
||||||
DeleteWithRevision(context.Context, string, uint) error
|
|
||||||
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
|
|
||||||
SaveResetOrder(context.Context, []model.FirewallRule) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleRepo struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
|
||||||
if err := prepareFirewallRule(rule); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.dbFor(ctx).Create(rule).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
|
||||||
var rule model.FirewallRule
|
|
||||||
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
|
||||||
return rule, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
|
||||||
var rules []model.FirewallRule
|
|
||||||
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
|
||||||
for _, opt := range opts {
|
|
||||||
db = opt(db)
|
|
||||||
}
|
|
||||||
return rules, db.Find(&rules).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
|
||||||
updates = sanitizeRuleUpdates(updates)
|
|
||||||
updates["revision"] = gorm.Expr("revision + 1")
|
|
||||||
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Updates(updates)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
|
||||||
result := r.dbFor(ctx).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Delete(&model.FirewallRule{})
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error {
|
|
||||||
failures := make(map[string]error)
|
|
||||||
for start := 0; start < len(rules); start += 500 {
|
|
||||||
batch := rules[start:min(start+500, len(rules))]
|
|
||||||
ids := make([][]interface{}, 0, len(batch))
|
|
||||||
for _, rule := range batch {
|
|
||||||
ids = append(ids, []interface{}{rule.UUID, rule.Revision})
|
|
||||||
failures[rule.UUID] = ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
var deleted []model.FirewallRule
|
|
||||||
err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}).
|
|
||||||
Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error
|
|
||||||
if err != nil {
|
|
||||||
for _, rule := range batch {
|
|
||||||
failures[rule.UUID] = err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, rule := range deleted {
|
|
||||||
delete(failures, rule.UUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return failures
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error {
|
|
||||||
if len(rules) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
for _, rule := range rules {
|
|
||||||
result := tx.Model(&model.FirewallRule{}).
|
|
||||||
Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision).
|
|
||||||
Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")})
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
|
||||||
return firewallDB(ctx, r.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
|
||||||
return tx.WithContext(ctx)
|
|
||||||
}
|
|
||||||
if fallback == nil {
|
|
||||||
fallback = global.DB
|
|
||||||
}
|
|
||||||
return fallback.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareFirewallRule(rule *model.FirewallRule) error {
|
|
||||||
if rule == nil {
|
|
||||||
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
|
||||||
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.UUID == "" {
|
|
||||||
rule.UUID = uuid.NewString()
|
|
||||||
}
|
|
||||||
if rule.Revision == 0 {
|
|
||||||
rule.Revision = 1
|
|
||||||
}
|
|
||||||
if rule.Origin == "" {
|
|
||||||
rule.Origin = constant.FirewallRuleOriginCreated
|
|
||||||
}
|
|
||||||
if rule.Owner == "" {
|
|
||||||
rule.Owner = constant.FirewallRuleSourceUser
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
|
||||||
result := make(map[string]interface{}, len(updates)+1)
|
|
||||||
for key, value := range updates {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
delete(result, "id")
|
|
||||||
delete(result, "uuid")
|
|
||||||
delete(result, "revision")
|
|
||||||
delete(result, "created_at")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IForwardingRuleRepo interface {
|
|
||||||
List(context.Context) ([]model.ForwardingRule, error)
|
|
||||||
CreateBatch(context.Context, []model.ForwardingRule) error
|
|
||||||
DeleteBatch(context.Context, []uint) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForwardingRuleRepo struct{}
|
|
||||||
|
|
||||||
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
|
||||||
var rules []model.ForwardingRule
|
|
||||||
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
|
||||||
return rules, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
|
|
||||||
if len(rules) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
|
|
||||||
}
|
|
||||||
+103
-13
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -10,11 +12,25 @@ import (
|
|||||||
|
|
||||||
type MonitorRepo struct{}
|
type MonitorRepo struct{}
|
||||||
|
|
||||||
|
type GPUHistoryPoint struct {
|
||||||
|
model.MonitorGPU
|
||||||
|
Bucket int64
|
||||||
|
PowerPercent *float64
|
||||||
|
MemoryPercent *float64
|
||||||
|
ProcessCount *float64
|
||||||
|
}
|
||||||
|
|
||||||
type IMonitorRepo interface {
|
type IMonitorRepo interface {
|
||||||
|
CleanHost() error
|
||||||
|
CleanGPU() error
|
||||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
CountGPU(opts ...DBOption) (int64, error)
|
||||||
|
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||||
|
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||||
|
GetIONames() ([]string, error)
|
||||||
|
GetNetworkNames() ([]string, error)
|
||||||
|
|
||||||
CreateMonitorBase(model model.MonitorBase) error
|
CreateMonitorBase(model model.MonitorBase) error
|
||||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
|||||||
DelMonitorIO(timeForDelete time.Time) error
|
DelMonitorIO(timeForDelete time.Time) error
|
||||||
DelMonitorNet(timeForDelete time.Time) error
|
DelMonitorNet(timeForDelete time.Time) error
|
||||||
|
|
||||||
WithByProductName(name string) DBOption
|
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIMonitorRepo() IMonitorRepo {
|
func NewIMonitorRepo() IMonitorRepo {
|
||||||
return &MonitorRepo{}
|
return &MonitorRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanHost() error {
|
||||||
|
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||||
|
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanGPU() error {
|
||||||
|
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||||
var data []model.MonitorBase
|
var data []model.MonitorBase
|
||||||
db := global.MonitorDB
|
db := global.MonitorDB
|
||||||
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
|||||||
err := db.Find(&data).Error
|
err := db.Find(&data).Error
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||||
var data []model.MonitorGPU
|
var names []string
|
||||||
db := global.GPUMonitorDB
|
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||||
for _, opt := range opts {
|
return names, err
|
||||||
db = opt(db)
|
}
|
||||||
}
|
|
||||||
err := db.Find(&data).Error
|
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||||
return data, err
|
var names []string
|
||||||
|
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||||
|
return names, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||||
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
|||||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
var data []model.MonitorGPU
|
||||||
return g.Where("product_name = ?", name)
|
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if deviceID != "" {
|
||||||
|
return db.Where("device_id = ?", deviceID)
|
||||||
|
}
|
||||||
|
db = db.Where("product_name = ?", name)
|
||||||
|
if legacy {
|
||||||
|
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||||
|
}
|
||||||
|
return db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
expressions := []string{
|
||||||
|
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||||
|
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||||
|
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||||
|
}
|
||||||
|
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||||
|
columns := []string{"*"}
|
||||||
|
if bucketSeconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||||
|
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
for i := range expressions {
|
||||||
|
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
for i, expression := range expressions {
|
||||||
|
columns = append(columns, expression+" AS "+aliases[i])
|
||||||
|
}
|
||||||
|
var data []GPUHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VLLMMonitorRepo struct{}
|
||||||
|
|
||||||
|
type VLLMHistoryPoint struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Bucket int64
|
||||||
|
CompletedRequests *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||||
|
return global.VLLMMonitorDB.Create(point).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||||
|
var point model.MonitorVLLM
|
||||||
|
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||||
|
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return point, nil
|
||||||
|
}
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||||
|
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||||
|
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
var metrics []string
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
metrics = append(metrics, "running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput")
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
metrics = append(metrics, "prefix_cache_hit_rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
if group == "" || group == "latency" {
|
||||||
|
metrics = append(metrics, "queue_time", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99")
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
metrics = append(metrics, "average_generation_tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var columns []string
|
||||||
|
if seconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||||
|
for _, column := range metrics {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||||
|
}
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
|
||||||
|
if seconds > 0 {
|
||||||
|
count = "SUM(" + count + ")"
|
||||||
|
}
|
||||||
|
columns = append(columns, count+" AS completed_requests")
|
||||||
|
}
|
||||||
|
var points []VLLMHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||||
|
return points, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) WalkDeltas(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string, visit func(int64, string, string) error) error {
|
||||||
|
histograms, requests := "''", "''"
|
||||||
|
if group == "" || group == "requests" || (group == "latency" && aggregation != "max") {
|
||||||
|
histograms = "COALESCE(histogram_deltas, '')"
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" || (group == "overview" && aggregation != "max") {
|
||||||
|
requests = "COALESCE(request_deltas, '')"
|
||||||
|
}
|
||||||
|
if histograms == "''" && requests == "''" {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
bucket := fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d", start.Unix(), seconds)
|
||||||
|
rows, err := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).
|
||||||
|
Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end).
|
||||||
|
Select(bucket + ", " + histograms + ", " + requests).Rows()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var index int64
|
||||||
|
var histograms, requests string
|
||||||
|
if err := rows.Scan(&index, &histograms, &requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visit(index, histograms, requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingRepo struct{}
|
type SettingRepo struct{}
|
||||||
@@ -14,17 +16,22 @@ type ISettingRepo interface {
|
|||||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||||
Get(opts ...DBOption) (model.Setting, error)
|
Get(opts ...DBOption) (model.Setting, error)
|
||||||
GetValueByKey(key string) (string, error)
|
GetValueByKey(key string) (string, error)
|
||||||
|
GetCoreValueByKey(key string) (string, error)
|
||||||
Create(key, value string) error
|
Create(key, value string) error
|
||||||
Update(key, value string) error
|
Update(key, value string) error
|
||||||
WithByKey(key string) DBOption
|
WithByKey(key string) DBOption
|
||||||
|
|
||||||
UpdateOrCreate(key, value string) error
|
UpdateOrCreate(key, value string) error
|
||||||
|
UpdateValues(map[string]string) error
|
||||||
|
|
||||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||||
CreateDescription(data *model.CommonDescription) error
|
CreateDescription(data *model.CommonDescription) error
|
||||||
|
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||||
UpdateDescription(id string, val map[string]interface{}) error
|
UpdateDescription(id string, val map[string]interface{}) error
|
||||||
DelDescription(id string) error
|
DelDescription(id string) error
|
||||||
|
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||||
|
WithDescriptionIDs(ids []string) DBOption
|
||||||
WithByDescriptionID(id string) DBOption
|
WithByDescriptionID(id string) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +75,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
|||||||
return setting.Value, nil
|
return setting.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
|
||||||
|
if global.CoreDB == nil {
|
||||||
|
return "", errors.New("core database is unavailable")
|
||||||
|
}
|
||||||
|
var setting model.Setting
|
||||||
|
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) WithByKey(key string) DBOption {
|
func (s *SettingRepo) WithByKey(key string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("key = ?", key)
|
return g.Where("key = ?", key)
|
||||||
@@ -90,6 +108,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
|||||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for key, value := range values {
|
||||||
|
var setting model.Setting
|
||||||
|
err := tx.Where("key = ?", key).First(&setting).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||||
var lists []model.CommonDescription
|
var lists []model.CommonDescription
|
||||||
db := global.DB.Model(&model.CommonDescription{})
|
db := global.DB.Model(&model.CommonDescription{})
|
||||||
@@ -111,14 +148,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
|||||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||||
return global.DB.Create(data).Error
|
return global.DB.Create(data).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||||
|
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||||
|
}).CreateInBatches(&descriptions, 100).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||||
}
|
}
|
||||||
func (s *SettingRepo) DelDescription(id string) error {
|
func (s *SettingRepo) DelDescription(id string) error {
|
||||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||||
|
var deleted int64
|
||||||
|
for start := 0; start < len(ids); start += 500 {
|
||||||
|
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||||
|
if emptyOnly {
|
||||||
|
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||||
|
}
|
||||||
|
result := query.Delete(&model.CommonDescription{})
|
||||||
|
deleted += result.RowsAffected
|
||||||
|
if result.Error != nil {
|
||||||
|
return deleted, result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("id = ?", id)
|
return g.Where("id = ?", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
||||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||||
|
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ const (
|
|||||||
|
|
||||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||||
agentType := req.AgentType
|
agentType := req.AgentType
|
||||||
|
if err := validateNativeAgentCreate(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
}
|
}
|
||||||
} else if agentType == constant.AppCopaw {
|
} else if agentType == constant.AppCopaw {
|
||||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
|
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||||
|
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
constant.HostIP: "",
|
constant.HostIP: "",
|
||||||
}
|
}
|
||||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||||
|
}
|
||||||
|
if agentType == constant.AppOctop {
|
||||||
|
params["OCTOP_LOG_LEVEL"] = "info"
|
||||||
|
}
|
||||||
if agentType == constant.AppOpenclaw {
|
if agentType == constant.AppOpenclaw {
|
||||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||||
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1044,7 +1055,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1097,12 +1108,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||||
requestedVerifyModel = account.VerifyModel
|
requestedVerifyModel = account.VerifyModel
|
||||||
}
|
}
|
||||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
verifyModel := ""
|
||||||
|
if account.AppInstallID == 0 || len(models) > 0 {
|
||||||
|
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1130,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
if err := global.DB.Save(account).Error; err != nil {
|
if err := agentAccountRepo.Save(account); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
@@ -1147,13 +1161,34 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
installIDs := make([]uint, 0, len(list))
|
||||||
|
for _, account := range list {
|
||||||
|
if account.AppInstallID != 0 {
|
||||||
|
installIDs = append(installIDs, account.AppInstallID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localInstalls := make(map[uint]bool, len(installIDs))
|
||||||
|
if len(installIDs) > 0 {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
localInstalls[install.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
if item.RememberAPIKey {
|
if item.RememberAPIKey {
|
||||||
apiKey = item.APIKey
|
apiKey = item.APIKey
|
||||||
}
|
}
|
||||||
|
appInstallID := uint(0)
|
||||||
|
if localInstalls[item.AppInstallID] {
|
||||||
|
appInstallID = item.AppInstallID
|
||||||
|
}
|
||||||
items = append(items, dto.AgentAccountInfo{
|
items = append(items, dto.AgentAccountInfo{
|
||||||
|
AppInstallID: appInstallID,
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
@@ -1351,13 +1386,27 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
agentAccountMutationMu.Lock()
|
agentAccountMutationMu.Lock()
|
||||||
defer agentAccountMutationMu.Unlock()
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if account.AppInstallID != 0 && account.Provider != "ollama" {
|
||||||
|
_, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||||
|
if err == nil {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "Delete the local inference engine instead", nil)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
var agentCount int64
|
var agentCount int64
|
||||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hermesWeixinLoginScript = `import asyncio
|
const hermesWeixinLoginScript = `import asyncio
|
||||||
from gateway.platforms.weixin import qr_login
|
try:
|
||||||
|
from plugins.platforms.weixin.adapter import qr_login
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
if exc.name not in ('plugins', 'plugins.platforms', 'plugins.platforms.weixin', 'plugins.platforms.weixin.adapter'):
|
||||||
|
raise
|
||||||
|
from gateway.platforms.weixin import qr_login
|
||||||
from hermes_cli.config import save_env_value
|
from hermes_cli.config import save_env_value
|
||||||
|
|
||||||
creds = asyncio.run(qr_login('/opt/data'))
|
creds = asyncio.run(qr_login('/opt/data'))
|
||||||
print('RESULT =', creds)
|
|
||||||
|
|
||||||
if not creds:
|
if not creds:
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"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/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
||||||
|
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||||
|
return nil, fmt.Errorf("invalid local model installation")
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||||
|
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||||
|
return nil, fmt.Errorf("model account belongs to another installation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if !createIfMissing {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
name := install.Name
|
||||||
|
for suffix := 1; ; suffix++ {
|
||||||
|
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(existing) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||||
|
}
|
||||||
|
apiType, apiKey := "openai-completions", "vllm-local"
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
apiType, apiKey = "openai-responses", "ollama-local"
|
||||||
|
}
|
||||||
|
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
||||||
|
}
|
||||||
|
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||||
|
account.AppInstallID = install.ID
|
||||||
|
if updateBaseURL {
|
||||||
|
account.BaseURL = baseURL
|
||||||
|
}
|
||||||
|
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||||
|
verifyModelExists := account.VerifyModel == ""
|
||||||
|
for _, item := range models {
|
||||||
|
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||||
|
if item.ID == account.VerifyModel {
|
||||||
|
verifyModelExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !verifyModelExists {
|
||||||
|
account.VerifyModel = ""
|
||||||
|
accountChanged = true
|
||||||
|
}
|
||||||
|
var previous []model.AgentAccountModel
|
||||||
|
if account.ID != 0 {
|
||||||
|
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelsChanged := len(previous) != len(rows)
|
||||||
|
if !modelsChanged {
|
||||||
|
for i := range rows {
|
||||||
|
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||||
|
modelsChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modelsChanged {
|
||||||
|
err = agentAccountRepo.SaveLocal(account, rows)
|
||||||
|
} else if accountChanged {
|
||||||
|
err = agentAccountRepo.Save(account)
|
||||||
|
} else {
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||||
|
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !install.SyncModelAccount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
|
||||||
|
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||||
|
for _, item := range models {
|
||||||
|
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||||
|
}
|
||||||
|
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||||
|
addressType := install.ModelAccountBaseURLType
|
||||||
|
if addressType == "" {
|
||||||
|
addressType = "container"
|
||||||
|
}
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return addressType, install.ModelAccountBaseURL, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if addressType != "custom" {
|
||||||
|
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
|
||||||
|
addressType = "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addressType, account.BaseURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupLocalModelAccount(installID uint) {
|
||||||
|
if installID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load local model accounts: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||||
|
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||||
|
switch addressType {
|
||||||
|
case "custom":
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||||
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||||
|
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
||||||
|
}
|
||||||
|
return baseURL, nil
|
||||||
|
case "localhost":
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||||
|
case "systemIP":
|
||||||
|
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
systemIP = strings.TrimSpace(systemIP)
|
||||||
|
if systemIP == "" {
|
||||||
|
systemIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
|
||||||
|
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
|
||||||
|
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var actual modelEntry
|
||||||
|
if err := json.Unmarshal(payload, &actual); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if actual.ID != id || actual.Name != "Saved name" {
|
||||||
|
t.Fatalf("changed saved identity: %s", payload)
|
||||||
|
}
|
||||||
|
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
|
||||||
|
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
|
||||||
|
t.Fatalf("incorrect direct Flash metadata: %s", payload)
|
||||||
|
}
|
||||||
|
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
|
||||||
|
t.Fatalf("changed existing/gateway limits: %s", payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
|
||||||
|
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
|
||||||
|
}}
|
||||||
|
conf := map[string]interface{}{}
|
||||||
|
if err := applyOpenclawModelsConfig(conf, current); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
|
||||||
|
}}
|
||||||
|
preserveOpenclawModelMetadata(conf, next)
|
||||||
|
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
|
||||||
|
t.Fatalf("saved metadata changed: %+v", next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
|
||||||
|
models, err := buildInitialAgentAccountModels(account, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
|
||||||
|
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
|
||||||
|
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
|
||||||
|
}
|
||||||
|
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
|
||||||
|
t.Fatalf("default request: %s, %v", request.Body, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxHermesConfiguration(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
|
||||||
|
dir := t.TempDir()
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
|
||||||
|
if err := writeHermesConfig(dir, account, id, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
|
||||||
|
t.Fatalf("changed Hermes selection: %+v", conf.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitAccountModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
|
||||||
|
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
|
||||||
|
actual, err := buildInitialAgentAccountModels(account, requested)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, requested) {
|
||||||
|
t.Fatalf("changed explicit %s models: %+v", provider, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||||
|
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
|
||||||
|
|
||||||
|
func isDeepSeekAccessHost(value string) bool {
|
||||||
|
if ip := net.ParseIP(value); ip != nil {
|
||||||
|
return ip.To4() != nil && !strings.Contains(value, ":")
|
||||||
|
}
|
||||||
|
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, label := range strings.Split(value, ".") {
|
||||||
|
if !deepSeekHostLabelPattern.MatchString(label) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
|
||||||
|
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
if req.AgentType == constant.AppDeepSeekHarness {
|
||||||
|
if !isDeepSeekAccessHost(req.AccessHost) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
|
||||||
|
}
|
||||||
|
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hasLetter, hasDigit := false, false
|
||||||
|
for _, char := range req.DashboardPassword {
|
||||||
|
hasLetter = hasLetter || unicode.IsLetter(char)
|
||||||
|
hasDigit = hasDigit || unicode.IsDigit(char)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
switch strings.ToLower(req.DashboardPassword) {
|
||||||
|
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
|
||||||
|
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedAPIType := strings.TrimSpace(apiType)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
@@ -113,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
modelID = strings.TrimSpace(modelID)
|
modelID = strings.TrimSpace(modelID)
|
||||||
if modelID == "" {
|
if modelID == "" && !allowEmptyModel {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
} else {
|
} else {
|
||||||
item.WebUIPort = appInstall.HttpPort
|
item.WebUIPort = appInstall.HttpPort
|
||||||
}
|
}
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
item.WebUIPort = appInstall.HttpsPort
|
||||||
|
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||||
|
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||||
|
if isDeepSeekAccessHost(accessHost) {
|
||||||
|
item.AccessHost = accessHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item.Path = appInstall.GetPath()
|
item.Path = appInstall.GetPath()
|
||||||
item.Status = appInstall.Status
|
item.Status = appInstall.Status
|
||||||
item.Message = appInstall.Message
|
item.Message = appInstall.Message
|
||||||
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||||
|
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||||
@@ -652,7 +662,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, item.Version)
|
versions = append(versions, item.Version)
|
||||||
@@ -660,12 +670,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(install.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||||
return install.App.CrossVersionUpdate
|
return install.App.CrossVersionUpdate
|
||||||
}
|
}
|
||||||
return common.CompareVersion(lastVersion, install.Version)
|
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawConfig struct {
|
type openclawConfig struct {
|
||||||
@@ -1253,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", modelEntry{}, "", modelProvider{}, err
|
return "", modelEntry{}, "", modelProvider{}, err
|
||||||
}
|
}
|
||||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
|
||||||
|
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
|
||||||
|
entry.ContextWindow = 1000000
|
||||||
|
entry.MaxTokens = 524288
|
||||||
|
}
|
||||||
|
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
|
||||||
ApiKey: providerPatch.APIKey,
|
ApiKey: providerPatch.APIKey,
|
||||||
BaseUrl: providerPatch.BaseURL,
|
BaseUrl: providerPatch.BaseURL,
|
||||||
Api: providerPatch.APIType,
|
Api: providerPatch.APIType,
|
||||||
@@ -1641,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
|||||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
case constant.AppCopaw:
|
case constant.AppCopaw:
|
||||||
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
case constant.AppDeepSeekHarness:
|
||||||
|
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||||
|
case constant.AppOctop:
|
||||||
|
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
|
||||||
default:
|
default:
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-47
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,13 +29,15 @@ import (
|
|||||||
|
|
||||||
type AIToolService struct{}
|
type AIToolService struct{}
|
||||||
|
|
||||||
|
var ollamaModelMu sync.Mutex
|
||||||
|
|
||||||
type IAIToolService interface {
|
type IAIToolService interface {
|
||||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||||
Create(req dto.OllamaModelName) error
|
Create(req dto.OllamaModelName) error
|
||||||
Close(name string) error
|
Close(name string) error
|
||||||
Recreate(req dto.OllamaModelName) error
|
Recreate(req dto.OllamaModelName) error
|
||||||
Delete(req dto.ForceDelete) error
|
Delete(req dto.ForceDelete) error
|
||||||
Sync() ([]dto.OllamaModelDropList, error)
|
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||||
LoadDetail(name string) (string, error)
|
LoadDetail(name string) (string, error)
|
||||||
BindDomain(req dto.OllamaBindDomain) error
|
BindDomain(req dto.OllamaBindDomain) error
|
||||||
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
||||||
@@ -46,6 +49,7 @@ func NewIAIToolService() IAIToolService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
||||||
|
syncErr := syncOllamaModels()
|
||||||
var options []repo.DBOption
|
var options []repo.DBOption
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
options = append(options, repo.WithByLikeName(req.Info))
|
options = append(options, repo.WithByLikeName(req.Info))
|
||||||
@@ -60,6 +64,11 @@ func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModel
|
|||||||
if err := copier.Copy(&item, &itemModel); err != nil {
|
if err := copier.Copy(&item, &itemModel); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if syncErr != nil && item.Status != constant.StatusWaiting {
|
||||||
|
item.Status = constant.StatusUnknown
|
||||||
|
item.Message = syncErr.Error()
|
||||||
|
item.Size = ""
|
||||||
|
}
|
||||||
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
||||||
if len(taskModel.ID) != 0 {
|
if len(taskModel.ID) != 0 {
|
||||||
item.LogFileExist = true
|
item.LogFileExist = true
|
||||||
@@ -85,6 +94,8 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -125,6 +136,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -144,6 +158,8 @@ func (u *AIToolService) Close(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -179,11 +195,16 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||||
if len(ollamaList) == 0 {
|
if len(ollamaList) == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
@@ -202,55 +223,28 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
|||||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||||
_ = os.Remove(logItem)
|
_ = os.Remove(logItem)
|
||||||
}
|
}
|
||||||
return nil
|
return SyncOllamaModelAccount(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||||
containerName, err := LoadContainerName()
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
if install.App.Key != "ollama" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
|
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var list []model.OllamaModel
|
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||||
lines := strings.Split(stdout, "\n")
|
return err
|
||||||
for _, line := range lines {
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if parts[0] == "NAME" {
|
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||||
continue
|
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||||
}
|
"model_account_base_url": baseURL,
|
||||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
}, repo.WithByID(install.ID))
|
||||||
}
|
|
||||||
listInDB, _ := aiRepo.List()
|
|
||||||
var dropList []dto.OllamaModelDropList
|
|
||||||
for _, itemModel := range listInDB {
|
|
||||||
isExit := false
|
|
||||||
for i := 0; i < len(list); i++ {
|
|
||||||
if list[i].Name == itemModel.Name {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
|
|
||||||
list = append(list[:i], list[(i+1):]...)
|
|
||||||
isExit = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isExit && itemModel.Status != constant.StatusWaiting {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
|
|
||||||
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range list {
|
|
||||||
item.Status = constant.StatusSuccess
|
|
||||||
item.From = "remote"
|
|
||||||
_ = aiRepo.Create(&item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||||
@@ -370,6 +364,72 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncOllamaModels() error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
|
containerName, err := LoadContainerName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
models := make(map[string]string)
|
||||||
|
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||||
|
if len(lines) == 0 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "NAME") {
|
||||||
|
return fmt.Errorf("invalid ollama list output: %s", stdout)
|
||||||
|
}
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 5 {
|
||||||
|
return fmt.Errorf("invalid ollama model entry: %s", line)
|
||||||
|
}
|
||||||
|
models[parts[0]] = parts[2] + " " + parts[3]
|
||||||
|
}
|
||||||
|
list, err := aiRepo.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
known := make(map[string]bool, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
name := item.Name
|
||||||
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
|
name += ":latest"
|
||||||
|
}
|
||||||
|
size, exists := models[name]
|
||||||
|
known[name] = true
|
||||||
|
if item.Status == constant.StatusWaiting {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message := constant.StatusSuccess, ""
|
||||||
|
if !exists {
|
||||||
|
if item.Status == constant.StatusFailed || item.Status == constant.StatusCanceled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message = constant.StatusDeleted, "not exist"
|
||||||
|
}
|
||||||
|
if item.Status != status || item.Size != size || item.Message != message {
|
||||||
|
if err := aiRepo.Update(item.ID, map[string]interface{}{"status": status, "size": size, "message": message}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, size := range models {
|
||||||
|
if known[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := model.OllamaModel{Name: name, Size: size, From: "remote", Status: constant.StatusSuccess}
|
||||||
|
if err := aiRepo.Create(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoadContainerName() (string, error) {
|
func LoadContainerName() (string, error) {
|
||||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -387,12 +447,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
lines := strings.Split(stdout, "\n")
|
lines := strings.Split(stdout, "\n")
|
||||||
for _, line := range lines {
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
if !strings.Contains(line, name) {
|
name += ":latest"
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
for _, line := range lines {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) < 5 {
|
if len(parts) < 5 || parts[0] != name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return parts[2] + " " + parts[3], nil
|
return parts[2] + " " + parts[3], nil
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
|
||||||
|
if kind != "vllm" && kind != "ollama" {
|
||||||
|
return nil, errors.New("unsupported benchmark source")
|
||||||
|
}
|
||||||
|
selectedID := strings.SplitN(selected, ":", 2)[0]
|
||||||
|
type instance struct {
|
||||||
|
id uint
|
||||||
|
name, container string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
instances := []instance{}
|
||||||
|
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results := []dto.AIBenchmarkSource{}
|
||||||
|
if len(instances) == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
|
||||||
|
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
for _, instance := range instances {
|
||||||
|
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
|
||||||
|
inspected, err := client.ContainerInspect(ctx, instance.container)
|
||||||
|
if err != nil {
|
||||||
|
base.ID = strconv.FormatUint(uint64(instance.id), 10)
|
||||||
|
base.Message = "container inspection unavailable"
|
||||||
|
results = append(results, base)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inspected.State != nil {
|
||||||
|
base.Status = inspected.State.Status
|
||||||
|
}
|
||||||
|
args := []string{}
|
||||||
|
if inspected.Config != nil {
|
||||||
|
args = append(args, inspected.Config.Entrypoint...)
|
||||||
|
args = append(args, inspected.Config.Cmd...)
|
||||||
|
}
|
||||||
|
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
|
||||||
|
parser := shellwords.NewParser()
|
||||||
|
parser.ParseEnv = false
|
||||||
|
parser.ParseBacktick = false
|
||||||
|
if parsed, err := parser.Parse(args[2]); err == nil {
|
||||||
|
args = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flags := map[string]string{}
|
||||||
|
for i, arg := range args {
|
||||||
|
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
|
||||||
|
flags[key] = value
|
||||||
|
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
|
||||||
|
flags[arg] = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name := flags["--served-model-name"]; name != "" {
|
||||||
|
base.Models = []string{name}
|
||||||
|
}
|
||||||
|
tokenizer := flags["--tokenizer"]
|
||||||
|
if tokenizer == "" {
|
||||||
|
tokenizer = flags["--model"]
|
||||||
|
for i, arg := range args {
|
||||||
|
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||||
|
tokenizer = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kind != "ollama" && tokenizer != "" {
|
||||||
|
if !filepath.IsAbs(tokenizer) {
|
||||||
|
base.Tokenizer = tokenizer
|
||||||
|
} else {
|
||||||
|
longest := 0
|
||||||
|
for _, mount := range inspected.Mounts {
|
||||||
|
relative, err := filepath.Rel(mount.Destination, tokenizer)
|
||||||
|
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
|
||||||
|
candidate := filepath.Join(mount.Source, relative)
|
||||||
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||||
|
base.Tokenizer = candidate
|
||||||
|
longest = len(mount.Destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ports := map[int]string{}
|
||||||
|
if instance.port > 0 {
|
||||||
|
ports[instance.port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
if inspected.NetworkSettings != nil {
|
||||||
|
for _, bindings := range inspected.NetworkSettings.Ports {
|
||||||
|
for _, binding := range bindings {
|
||||||
|
port, _ := strconv.Atoi(binding.HostPort)
|
||||||
|
if port > 0 {
|
||||||
|
host := binding.HostIP
|
||||||
|
if host == "" || host == "0.0.0.0" || host == "::" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
ports[port] = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
|
||||||
|
port, _ := strconv.Atoi(flags["--port"])
|
||||||
|
if port > 0 {
|
||||||
|
ports[port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered := []int{}
|
||||||
|
for port := range ports {
|
||||||
|
ordered = append(ordered, port)
|
||||||
|
}
|
||||||
|
sort.Ints(ordered)
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
ordered = append(ordered, 0)
|
||||||
|
}
|
||||||
|
for _, port := range ordered {
|
||||||
|
source := base
|
||||||
|
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
|
||||||
|
if selected != "" && source.ID != selected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port > 0 {
|
||||||
|
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
|
||||||
|
endpoint := "/v1/models"
|
||||||
|
if kind == "ollama" {
|
||||||
|
endpoint = "/api/tags"
|
||||||
|
}
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
source.Message = "model list unavailable; enter the served model name"
|
||||||
|
} else {
|
||||||
|
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode == http.StatusOK && readErr == nil {
|
||||||
|
var data struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
Models []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"models"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &data); err == nil {
|
||||||
|
source.Models = []string{}
|
||||||
|
for _, model := range data.Data {
|
||||||
|
source.Models = append(source.Models, model.ID)
|
||||||
|
}
|
||||||
|
for _, model := range data.Models {
|
||||||
|
source.Models = append(source.Models, model.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAIGatewayAppRecover(install *model.AppInstall, recoverFile, secret string) (string, func(), error) {
|
||||||
|
noop := func() {}
|
||||||
|
if !isAIGatewayApp(install.App.Key) {
|
||||||
|
return "", noop, nil
|
||||||
|
}
|
||||||
|
tmpDir, err := os.MkdirTemp(global.Dir.TmpDir, "ai-gateway-recover-")
|
||||||
|
if err != nil {
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
cleanup := func() { _ = os.RemoveAll(tmpDir) }
|
||||||
|
fail := func(err error) (string, func(), error) {
|
||||||
|
cleanup()
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
if err := files.NewFileOp().TarGzExtractPro(recoverFile, tmpDir, secret); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
backupDir := filepath.Join(tmpDir, strings.ReplaceAll(filepath.Base(recoverFile), ".tar.gz", ""))
|
||||||
|
for _, name := range []string{"app.json", "app.tar.gz"} {
|
||||||
|
info, err := os.Lstat(filepath.Join(backupDir, name))
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata, err := os.ReadFile(filepath.Join(backupDir, "app.json"))
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
var backup model.AppInstall
|
||||||
|
if err := json.Unmarshal(metadata, &backup); err != nil {
|
||||||
|
return fail(fmt.Errorf("unmarshal app.json failed: %w", err))
|
||||||
|
}
|
||||||
|
if backup.App.Key != install.App.Key || backup.Name != install.Name {
|
||||||
|
return fail(buserr.New("AppAttributesNotMatch"))
|
||||||
|
}
|
||||||
|
// Historical backups already include Version. Unsuffixed releases are
|
||||||
|
// SQLite; no new metadata field is required. Missing versions are invalid,
|
||||||
|
// not evidence that an archive contains SQLite data.
|
||||||
|
if strings.TrimSpace(backup.Version) == "" || strings.TrimSpace(install.Version) == "" {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
// Deliberately do not compare version order: restoring an older version of
|
||||||
|
// the same database type is required for failed-upgrade rollbacks.
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, backup.Version) {
|
||||||
|
return fail(buserr.New("ErrAIGatewayRecoverDatabaseTypeMismatch"))
|
||||||
|
}
|
||||||
|
return backupDir, cleanup, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const aiGatewayAppKey = "ai-gateway"
|
||||||
|
|
||||||
|
func isAIGatewayApp(appKey string) bool {
|
||||||
|
return appKey == aiGatewayAppKey || appKey == "local"+aiGatewayAppKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAIGatewayPostgresVersion(version string) bool {
|
||||||
|
return strings.HasSuffix(version, "-pg") || strings.HasSuffix(version, "-postgres")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database variants are deployment choices, not interchangeable upgrades.
|
||||||
|
func isAIGatewayUpgradeVersionAllowed(appKey, currentVersion, targetVersion string) bool {
|
||||||
|
return !isAIGatewayApp(appKey) ||
|
||||||
|
isAIGatewayPostgresVersion(currentVersion) == isAIGatewayPostgresVersion(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAIGatewayVersions(versions []string) {
|
||||||
|
sort.SliceStable(versions, func(i, j int) bool {
|
||||||
|
if common.CompareVersion(versions[i], versions[j]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if common.CompareVersion(versions[j], versions[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Prefer SQLite only when the numeric versions are equal.
|
||||||
|
return !isAIGatewayPostgresVersion(versions[i]) && isAIGatewayPostgresVersion(versions[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
+135
-135
@@ -4,11 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -17,14 +14,13 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertService struct{}
|
type AlertService struct{}
|
||||||
@@ -78,10 +74,10 @@ type IAlertService interface {
|
|||||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) (string, error)
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -109,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cronjobProjects := make(map[string]uint)
|
||||||
|
var cronjobIDs []uint
|
||||||
for _, item := range alerts {
|
for _, item := range alerts {
|
||||||
|
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := cronjobProjects[item.Project]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||||
|
if parseErr != nil || id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cronjobProjects[item.Project] = uint(id)
|
||||||
|
cronjobIDs = append(cronjobIDs, uint(id))
|
||||||
|
}
|
||||||
|
cronjobsByID := make(map[uint]model.Cronjob)
|
||||||
|
if len(cronjobIDs) > 0 {
|
||||||
|
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, cronjob := range cronjobs {
|
||||||
|
cronjobsByID[cronjob.ID] = cronjob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range alerts {
|
||||||
|
var taskName string
|
||||||
|
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||||
|
taskName = cronjob.Name
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, dto.AlertDTO{
|
result = append(result, dto.AlertDTO{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
@@ -119,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
Method: item.Method,
|
Method: item.Method,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Project: item.Project,
|
Project: item.Project,
|
||||||
|
TaskName: taskName,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
SendCount: item.SendCount,
|
SendCount: item.SendCount,
|
||||||
AdvancedParams: item.AdvancedParams,
|
AdvancedParams: item.AdvancedParams,
|
||||||
@@ -190,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
create.AdvancedParams = advanced
|
||||||
|
if create.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
alertInfo.Status = constant.AlertEnable
|
alertInfo.Status = constant.AlertEnable
|
||||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||||
return buserr.WithErr("ErrStructTransform", err)
|
return buserr.WithErr("ErrStructTransform", err)
|
||||||
@@ -207,11 +245,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||||
|
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||||
|
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.Status != constant.AlertDisable {
|
if req.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,6 +329,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if status == constant.AlertEnable {
|
if status == constant.AlertEnable {
|
||||||
|
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -299,120 +353,12 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||||
var disks []dto.DiskDTO
|
infos := loadDiskInfo(true)
|
||||||
excludes := map[string]struct{}{
|
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
for _, item := range infos {
|
||||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
disks = append(disks, dto.DiskDTO(item))
|
||||||
}
|
}
|
||||||
stdout, err := executeDiskCommand()
|
|
||||||
if err != nil {
|
|
||||||
return disks, nil
|
return disks, nil
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(stdout, "\n")
|
|
||||||
var mounts []dto.AlertDiskInfo
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mountPoint := strings.Join(fields[6:], " ")
|
|
||||||
if shouldExclude(fields, mountPoint, excludes) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
wg.Add(len(mounts))
|
|
||||||
for i := 0; i < len(mounts); i++ {
|
|
||||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
var itemData dto.DiskDTO
|
|
||||||
itemData.Path = mount.Mount
|
|
||||||
itemData.Type = mount.Type
|
|
||||||
itemData.Device = mount.Device
|
|
||||||
select {
|
|
||||||
case <-timeoutCh:
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
|
||||||
default:
|
|
||||||
state, err := disk.Usage(mount.Mount)
|
|
||||||
if err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemData.Total = state.Total
|
|
||||||
itemData.Free = state.Free
|
|
||||||
itemData.Used = state.Used
|
|
||||||
itemData.UsedPercent = state.UsedPercent
|
|
||||||
itemData.InodesTotal = state.InodesTotal
|
|
||||||
itemData.InodesUsed = state.InodesUsed
|
|
||||||
itemData.InodesFree = state.InodesFree
|
|
||||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(time.After(5*time.Second), mounts[i])
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
sort.Slice(disks, func(i, j int) bool {
|
|
||||||
return disks[i].Path < disks[j].Path
|
|
||||||
})
|
|
||||||
return disks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeDiskCommand() (string, error) {
|
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
|
||||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
|
||||||
if err != nil {
|
|
||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
|
||||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return stdout, err
|
|
||||||
}
|
|
||||||
var lines []string
|
|
||||||
for _, line := range strings.Split(stdout, "\n") {
|
|
||||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
|
||||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(fields[2], "K") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(mountPoint, "docker") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, excluded := excludes[mountPoint]
|
|
||||||
return excluded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||||
@@ -677,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
|
||||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
config, err := alertRepo.GetConfigById(req.ID)
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
if req.Status == constant.AlertEnable {
|
if req.Status == constant.AlertEnable {
|
||||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if config.Type == constant.Custom {
|
if config.Type == constant.Custom {
|
||||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
"update_user": operator,
|
"update_user": operator,
|
||||||
}, repo.WithByID(req.ID))
|
}, repo.WithByID(req.ID))
|
||||||
@@ -867,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
|
|||||||
return strings.TrimSpace(cfg.Phone)
|
return strings.TrimSpace(cfg.Phone)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) DeleteAlertConfig(id uint) error {
|
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
|
||||||
_, err := alertRepo.GetConfigById(id)
|
config, err := alertRepo.GetConfigById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if len(usedAlerts) > 0 {
|
if len(usedAlerts) > 0 {
|
||||||
return buserr.New("ErrAlertConfigInUse")
|
return name, buserr.New("ErrAlertConfigInUse")
|
||||||
}
|
}
|
||||||
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||||
@@ -905,7 +853,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
||||||
Body: i18n.GetMsgByKey("TestAlert"),
|
Body: i18n.GetMsgByKey("TestAlertMessage"),
|
||||||
IsHTML: false,
|
IsHTML: false,
|
||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
@@ -953,7 +901,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
|
|||||||
return emailConfig, nil
|
return emailConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error) {
|
||||||
if req.Type != constant.Custom {
|
if req.Type != constant.Custom {
|
||||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||||
}
|
}
|
||||||
@@ -982,12 +930,15 @@ func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertC
|
|||||||
}
|
}
|
||||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
|
||||||
|
}
|
||||||
return dto.AlertConfigTestResult{
|
return dto.AlertConfigTestResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return tester.TestCustomWebhook(resolved)
|
return tester.TestCustomWebhook(resolved, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
@@ -1021,6 +972,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
alertRepo.WithByType(updateAlert.Type),
|
alertRepo.WithByType(updateAlert.Type),
|
||||||
alertRepo.WithByProject(updateAlert.Project),
|
alertRepo.WithByProject(updateAlert.Project),
|
||||||
)
|
)
|
||||||
|
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateAlert.AdvancedParams = advanced
|
||||||
|
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||||
|
upMap["advanced_params"] = advanced
|
||||||
|
}
|
||||||
|
if newStatus == constant.AlertEnable {
|
||||||
|
method := updateAlert.Method
|
||||||
|
if method == "" {
|
||||||
|
method = alertInfo.Method
|
||||||
|
}
|
||||||
|
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if alertInfo.ID > 0 {
|
if alertInfo.ID > 0 {
|
||||||
shouldUpdate := false
|
shouldUpdate := false
|
||||||
@@ -1034,6 +1002,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||||
shouldUpdate = true
|
shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
if shouldUpdate {
|
if shouldUpdate {
|
||||||
if err := alertRepo.Update(
|
if err := alertRepo.Update(
|
||||||
@@ -1055,3 +1026,32 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return incoming, nil
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||||
|
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.validateAlertMethodReferences(method)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func alertConfigLogName(config, title string) string {
|
||||||
|
var value struct {
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
|
||||||
|
return strings.TrimSpace(value.DisplayName)
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|||||||
@@ -530,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||||
}
|
}
|
||||||
|
interfaceAddrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||||
|
}
|
||||||
count, records := summarizeSSHLoginHistories(
|
count, records := summarizeSSHLoginHistories(
|
||||||
histories,
|
histories,
|
||||||
now,
|
now,
|
||||||
failedWindow,
|
failedWindow,
|
||||||
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
|
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||||
)
|
)
|
||||||
isAlert := count >= int(alert.Count)
|
isAlert := count >= int(alert.Count)
|
||||||
if isAlert {
|
if isAlert {
|
||||||
@@ -570,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||||
|
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||||
|
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||||
|
for _, addr := range interfaceAddrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
whitelist = append(whitelist, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
return whitelist
|
||||||
|
}
|
||||||
|
|
||||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||||
filtered := make([]model.LoginLog, 0, len(records))
|
filtered := make([]model.LoginLog, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -1049,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
|||||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var errMsgs []string
|
|
||||||
for _, item := range diskList {
|
for _, item := range diskList {
|
||||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
if item.Total == 0 {
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
|
||||||
errMsgs = append(errMsgs, errMsg)
|
|
||||||
global.LOG.Errorf("%s", errMsg)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||||
if len(errMsgs) > 0 {
|
|
||||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processSingleDisk(alert dto.AlertDTO) error {
|
func processSingleDisk(alert dto.AlertDTO) error {
|
||||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("%s", err.Error())
|
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||||
commonTotal := float64(alert.Count)
|
commonTotal := float64(alert.Count)
|
||||||
if alert.Cycle == 1 {
|
if alert.Cycle == 1 {
|
||||||
commonTotal *= 1024 * 1024 * 1024
|
commonTotal *= 1024 * 1024 * 1024
|
||||||
}
|
}
|
||||||
if usedTotal < commonTotal {
|
if usedTotal < commonTotal {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
params := createAlertDiskParams(path, usedStr)
|
params := createAlertDiskParams(path, usedStr)
|
||||||
sender := NewAlertSender(alert, alert.Project)
|
sender := NewAlertSender(alert, alert.Project)
|
||||||
sender.ResourceSend(path, params)
|
sender.ResourceSend(path, params)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||||
|
|||||||
@@ -258,13 +258,22 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||||
if !fileOp.Stat(versionPath) || detail.Update {
|
|
||||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
|
if err == nil && app.Resource == constant.AppResourceRemote {
|
||||||
|
detail.Update = false
|
||||||
|
appDetailDTO.Update = false
|
||||||
}
|
}
|
||||||
switch app.Type {
|
switch app.Type {
|
||||||
case constant.RuntimePHP:
|
case constant.RuntimePHP:
|
||||||
|
paramMap := make(map[string]interface{})
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
if err = json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
appDetailDTO.Params = paramMap
|
||||||
|
} else {
|
||||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||||
if !fileOp.Stat(paramsPath) {
|
if !fileOp.Stat(paramsPath) {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||||
@@ -273,11 +282,11 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
paramMap := make(map[string]interface{})
|
|
||||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||||
|
}
|
||||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||||
if !fileOp.Stat(composePath) {
|
if !fileOp.Stat(composePath) {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||||
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if appDetailDTO.DockerCompose == "" {
|
if appDetailDTO.DockerCompose == "" {
|
||||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
}
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||||
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
} else {
|
} else {
|
||||||
if appDetail.DockerCompose == "" {
|
if appDetail.DockerCompose == "" {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
var statusCode int
|
var statusCode int
|
||||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
@@ -478,6 +499,22 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
req.Params[constant.ContainerName] = containerName
|
req.Params[constant.ContainerName] = containerName
|
||||||
appInstall.ContainerName = containerName
|
appInstall.ContainerName = containerName
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
appInstall.SyncModelAccount = req.SyncModelAccount == nil || *req.SyncModelAccount
|
||||||
|
appInstall.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
if appInstall.ModelAccountBaseURLType == "" {
|
||||||
|
appInstall.ModelAccountBaseURLType = "container"
|
||||||
|
}
|
||||||
|
appInstall.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
}
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index := 0
|
index := 0
|
||||||
serviceName := ""
|
serviceName := ""
|
||||||
for k := range servicesMap {
|
for k := range servicesMap {
|
||||||
@@ -550,6 +587,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
|
|
||||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
appInstall.Status = constant.StatusInstallErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
|
||||||
|
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +599,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
installApp := func(t *task.Task) error {
|
installApp := func(t *task.Task) (err error) {
|
||||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -583,6 +625,10 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateToolApp(appInstall)
|
updateToolApp(appInstall)
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.ContainerName = appInstall.ContainerName
|
res.ContainerName = appInstall.ContainerName
|
||||||
|
res.ModelAccountBaseURLType = appInstall.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = appInstall.ModelAccountBaseURL
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(appInstall)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
res.Name = appInstall.Name
|
res.Name = appInstall.Name
|
||||||
res.Version = appInstall.Version
|
res.Version = appInstall.Version
|
||||||
res.CreatedAt = appInstall.CreatedAt
|
res.CreatedAt = appInstall.CreatedAt
|
||||||
@@ -339,6 +347,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
|
|
||||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
install.Status = constant.StatusUpErr
|
||||||
|
install.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
|
||||||
|
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operation := string(req.Operate)
|
operation := string(req.Operate)
|
||||||
@@ -433,6 +446,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if installed.App.Key == "ollama" {
|
||||||
|
if req.SyncModelAccount != nil {
|
||||||
|
installed.SyncModelAccount = *req.SyncModelAccount
|
||||||
|
}
|
||||||
|
if req.ModelAccountBaseURLType != "" {
|
||||||
|
installed.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
installed.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
} else if installed.SyncModelAccount {
|
||||||
|
installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(oldInstalled)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if installed.SyncModelAccount {
|
||||||
|
containerName := installed.ContainerName
|
||||||
|
if req.Advanced && req.ContainerName != "" {
|
||||||
|
containerName = req.ContainerName
|
||||||
|
}
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, containerName, "11434", installed.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backupDockerCompose := installed.DockerCompose
|
backupDockerCompose := installed.DockerCompose
|
||||||
if req.Advanced {
|
if req.Advanced {
|
||||||
composeMap := make(map[string]interface{})
|
composeMap := make(map[string]interface{})
|
||||||
@@ -558,6 +597,9 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
||||||
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
||||||
}
|
}
|
||||||
|
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
||||||
|
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,11 +609,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, i := range allList {
|
for _, i := range allList {
|
||||||
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
|
if appInstallOperationInterruptedOnRestart(i.Status) {
|
||||||
if systemInit {
|
if systemInit {
|
||||||
i.Status = constant.StatusError
|
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||||
i.Message = "1Panel restart causes the task to terminate"
|
i.Message = constant.InterruptedMsg
|
||||||
_ = appInstallRepo.Save(context.Background(), &i)
|
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := syncAppInstallStatus(&i, false); err != nil {
|
||||||
|
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -678,14 +724,17 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, detail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
|
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
canUpgrade := common.CompareVersion(detail.Version, install.Version)
|
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
|
||||||
if app.Key == vllmAppKeyForUpgrade {
|
if app.Key == vllmAppKeyForUpgrade {
|
||||||
currentImage := loadVllmImageFromEnv(install.Env)
|
currentImage := loadVllmImageFromEnv(install.Env)
|
||||||
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
||||||
@@ -695,6 +744,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
||||||
filename := filepath.Base(detail.DownloadUrl)
|
filename := filepath.Base(detail.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return versions, err
|
||||||
|
}
|
||||||
|
}
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
return versions, buserr.New("ErrAppVersionUnavailable")
|
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||||
@@ -726,7 +781,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
return common.CompareVersion(versions[i].Version, versions[j].Version)
|
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
|
||||||
})
|
})
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
@@ -916,6 +971,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
config := getAppCommonConfig(envs)
|
config := getAppCommonConfig(envs)
|
||||||
config.DockerCompose = install.DockerCompose
|
config.DockerCompose = install.DockerCompose
|
||||||
res.Params = params
|
res.Params = params
|
||||||
|
res.AppKey = install.App.Key
|
||||||
|
res.Name = install.Name
|
||||||
|
res.HttpPort = install.HttpPort
|
||||||
|
res.SyncModelAccount = install.SyncModelAccount
|
||||||
|
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = install.ModelAccountBaseURL
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if config.ContainerName == "" {
|
if config.ContainerName == "" {
|
||||||
config.ContainerName = install.ContainerName
|
config.ContainerName = install.ContainerName
|
||||||
}
|
}
|
||||||
@@ -931,10 +998,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
|
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 {
|
switch appInstall.Status {
|
||||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
|
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||||
|
if !force {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareMCPWebsiteDirectory(req request.AppInstallCreate) error {
|
||||||
|
changed := errors.New("MCP website directory changed; inspect the installation before retrying")
|
||||||
|
directory, ok := req.Params["WEBSITE_DIR"].(string)
|
||||||
|
if !ok || directory == "" || global.DB == nil {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var setting model.Setting
|
||||||
|
err := tx.Where("key = ?", "WEBSITE_DIR").First(&setting).Error
|
||||||
|
missing := errors.Is(err, gorm.ErrRecordNotFound)
|
||||||
|
if err != nil && !missing {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := setting.Value
|
||||||
|
if current == "" {
|
||||||
|
current = path.Join(global.Dir.BaseDir, "1panel", "www")
|
||||||
|
}
|
||||||
|
if directory != current {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
if missing {
|
||||||
|
if err := tx.Create(&model.Setting{Key: "WEBSITE_DIR", Value: directory}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if setting.Value == "" {
|
||||||
|
result := tx.Model(&model.Setting{}).Where("id = ? AND value = ?", setting.ID, "").Update("value", directory)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected != 1 {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(path.Join(directory, "conf.d"), constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func appInstallOperationFailureStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
return constant.StatusInstallErr
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
return constant.StatusUpgradeErr
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
return constant.StatusError
|
||||||
|
default:
|
||||||
|
return constant.StatusUpErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationPending(status string) bool {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationInterruptedOnRestart(status string) bool {
|
||||||
|
// ReStarting can also describe a container's Docker restart policy.
|
||||||
|
return appInstallOperationPending(status) && status != constant.StatusRestarting
|
||||||
|
}
|
||||||
|
|
||||||
|
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
|
||||||
|
if !appInstallOperationPending(install.Status) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var operationTask model.Task
|
||||||
|
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
|
||||||
|
Order("created_at DESC").First(&operationTask).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// A previous operation must not turn a newly queued retry into a failure.
|
||||||
|
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
|
||||||
|
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
message := operationTask.ErrorMsg
|
||||||
|
if message == "" {
|
||||||
|
message = "the application operation task failed"
|
||||||
|
}
|
||||||
|
status := appInstallOperationFailureStatus(install.Status)
|
||||||
|
result := appDB.Model(&model.AppInstall{}).
|
||||||
|
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
|
||||||
|
Updates(map[string]interface{}{"status": status, "message": message})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected > 0 {
|
||||||
|
install.Status = status
|
||||||
|
install.Message = message
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
|
||||||
|
t.Helper()
|
||||||
|
open := func(name string, schema interface{}) *gorm.DB {
|
||||||
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
if sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
appDB := open("apps.db", &model.AppInstall{})
|
||||||
|
taskDB := open("tasks.db", &model.Task{})
|
||||||
|
install := model.AppInstall{
|
||||||
|
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
|
||||||
|
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||||
|
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
|
||||||
|
}
|
||||||
|
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return appDB, taskDB, install
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
|
||||||
|
t.Run(state, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
install.Status = state
|
||||||
|
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := constant.StatusUpErr
|
||||||
|
if state == constant.StatusInstalling {
|
||||||
|
want = constant.StatusInstallErr
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
|
||||||
|
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
|
||||||
|
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
|
||||||
|
t.Run(taskStatus, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if taskStatus == "previous-failure" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
|
||||||
|
task.EndAt = install.UpdatedAt.Add(-time.Hour)
|
||||||
|
}
|
||||||
|
if taskStatus == "other-resource" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.ResourceID++
|
||||||
|
}
|
||||||
|
if taskStatus != "missing" {
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != constant.StatusStarting {
|
||||||
|
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
|
||||||
|
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
|
||||||
|
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
|
||||||
|
if !appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery omitted %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
|
||||||
|
if appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,7 +191,11 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
|
|||||||
|
|
||||||
if _, ok := InitTypes[app.Type]; ok {
|
if _, ok := InitTypes[app.Type]; ok {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
|
||||||
_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
dockerComposeUrl, err := appDownloadURL(dockerComposeUrl, strconv.Itoa(c.list.LastModified))
|
||||||
|
var composeRes []byte
|
||||||
|
if err == nil {
|
||||||
|
_, composeRes, err = req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
detail.DockerCompose = string(composeRes)
|
detail.DockerCompose = string(composeRes)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, detail.Version) {
|
||||||
|
return buserr.New("ErrAIGatewayDatabaseTypeMismatch")
|
||||||
|
}
|
||||||
|
if isHermesApp(install.App.Key) &&
|
||||||
|
(!isNewerAppVersion(install.App.Key, detail.Version, install.Version) ||
|
||||||
|
(isCrossAppVersion(install.App.Key, install.Version, detail.Version) && !install.App.CrossVersionUpdate)) {
|
||||||
|
return buserr.New("ErrHermesUpgradeVersion")
|
||||||
|
}
|
||||||
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -390,12 +391,27 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
|
}
|
||||||
tx, ctx := helper.GetTxAndContext()
|
tx, ctx := helper.GetTxAndContext()
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appKey := install.App.Key
|
appKey := install.App.Key
|
||||||
|
if appKey == "ollama" {
|
||||||
|
remaining, listErr := appInstallRepo.ListBy(ctx, appInstallRepo.WithAppId(install.AppId))
|
||||||
|
if listErr != nil {
|
||||||
|
return listErr
|
||||||
|
}
|
||||||
|
if len(remaining) == 0 {
|
||||||
|
if err = aiRepo.DeleteAll(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if isAgentAppKey(appKey) {
|
if isAgentAppKey(appKey) {
|
||||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||||
}
|
}
|
||||||
@@ -468,7 +484,12 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
if err == nil && len(entries) == 0 {
|
if err == nil && len(entries) == 0 {
|
||||||
_ = op.DeleteDir(parentDir)
|
_ = op.DeleteDir(parentDir)
|
||||||
}
|
}
|
||||||
tx.Commit()
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||||
|
CleanupLocalModelAccount(install.ID)
|
||||||
|
}
|
||||||
|
|
||||||
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
||||||
if len(existApps) == 0 {
|
if len(existApps) == 0 {
|
||||||
@@ -891,16 +912,52 @@ func handleMap(params map[string]interface{}, envParams map[string]string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appDownloadURL(rawURL, version string) (string, error) {
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
query := parsedURL.Query()
|
||||||
|
query.Set("v", version)
|
||||||
|
parsedURL.RawQuery = query.Encode()
|
||||||
|
return parsedURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentAppDownloadURL(rawURL string) (string, error) {
|
||||||
|
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return appDownloadURL(rawURL, storeVersion)
|
||||||
|
}
|
||||||
|
|
||||||
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
|
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
|
||||||
if app.IsLocalApp() || app.IsCustomApp() {
|
if app.IsLocalApp() || app.IsCustomApp() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil && appInstall != nil {
|
||||||
|
appInstall.Status = constant.StatusDownloadErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
}
|
||||||
|
}()
|
||||||
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
|
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
|
||||||
appDownloadDir := app.GetAppResourcePath()
|
appDownloadDir := app.GetAppResourcePath()
|
||||||
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheVersionPath := path.Join(appVersionDir, ".1panel-appstore-version")
|
||||||
if !appDetail.Update && fileOp.Stat(appVersionDir) {
|
if !appDetail.Update && fileOp.Stat(appVersionDir) {
|
||||||
return
|
if cachedVersion, readErr := os.ReadFile(cacheVersionPath); readErr == nil && string(cachedVersion) == storeVersion {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadURL, err := appDownloadURL(appDetail.DownloadUrl, storeVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if !fileOp.Stat(appDownloadDir) {
|
if !fileOp.Stat(appDownloadDir) {
|
||||||
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
|
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
|
||||||
@@ -909,23 +966,14 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
|||||||
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
|
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
|
||||||
}
|
}
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
global.LOG.Infof("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
global.LOG.Infof("download app [%s] from %s", app.Name, downloadURL)
|
||||||
} else {
|
} else {
|
||||||
logger.Printf("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
logger.Printf("download app [%s] from %s", app.Name, downloadURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
|
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
|
||||||
|
|
||||||
defer func() {
|
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
|
||||||
if err != nil {
|
|
||||||
if appInstall != nil {
|
|
||||||
appInstall.Status = constant.StatusDownloadErr
|
|
||||||
appInstall.Message = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -942,6 +990,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = fileOp.DeleteFile(filePath)
|
_ = fileOp.DeleteFile(filePath)
|
||||||
|
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
appDetail.Update = false
|
appDetail.Update = false
|
||||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
_ = appDetailRepo.Update(context.Background(), appDetail)
|
||||||
return
|
return
|
||||||
@@ -1406,9 +1457,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doNotNeedSync(installed model.AppInstall) bool {
|
func doNotNeedSync(installed model.AppInstall) bool {
|
||||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
|
||||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
|
||||||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||||
@@ -1502,6 +1552,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, installed := range appInstallList {
|
for _, installed := range appInstallList {
|
||||||
|
if sync && appInstallOperationPending(installed.Status) {
|
||||||
|
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if updated && ignoreUpdate(installed) {
|
if updated && ignoreUpdate(installed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1567,11 +1622,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
|
|
||||||
var versions []string
|
var versions []string
|
||||||
for _, appDetail := range details {
|
for _, appDetail := range details {
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, appDetail.Version)
|
versions = append(versions, appDetail.Version)
|
||||||
@@ -1586,7 +1644,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(installed.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
|
|
||||||
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
||||||
@@ -1601,10 +1659,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.IsCrossVersion(installed.Version, lastVersion) {
|
if isCrossAppVersion(installed.App.Key, installed.Version, lastVersion) {
|
||||||
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
||||||
} else {
|
} else {
|
||||||
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version)
|
installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if updated {
|
if updated {
|
||||||
@@ -1977,6 +2035,9 @@ func getAppTags(appID uint, lang string) ([]response.TagDTO, error) {
|
|||||||
|
|
||||||
func handleSiteDir(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task) error {
|
func handleSiteDir(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task) error {
|
||||||
if app.Key == "openresty" && (app.Resource == "remote" || app.Resource == "custom") && common.CompareVersion(appDetail.Version, "1.27") {
|
if app.Key == "openresty" && (app.Resource == "remote" || app.Resource == "custom") && common.CompareVersion(appDetail.Version, "1.27") {
|
||||||
|
if req.MCPPreserveWebsiteDir {
|
||||||
|
return prepareMCPWebsiteDirectory(req)
|
||||||
|
}
|
||||||
if dir, ok := req.Params["WEBSITE_DIR"]; ok {
|
if dir, ok := req.Params["WEBSITE_DIR"]; ok {
|
||||||
siteDir := dir.(string)
|
siteDir := dir.(string)
|
||||||
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
||||||
@@ -2275,7 +2336,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
|||||||
}
|
}
|
||||||
versionsRaw = append(versionsRaw, detail.Version)
|
versionsRaw = append(versionsRaw, detail.Version)
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versionsRaw)
|
if isAIGatewayApp(key) {
|
||||||
|
sortAIGatewayVersions(versionsRaw)
|
||||||
|
versions = versionsRaw
|
||||||
|
} else {
|
||||||
|
versions = sortAppVersions(key, versionsRaw)
|
||||||
|
}
|
||||||
if hasLatest {
|
if hasLatest {
|
||||||
versions = append([]string{latestVersion}, versions...)
|
versions = append([]string{latestVersion}, versions...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
|||||||
if !fileOp.Stat(req.File) {
|
if !fileOp.Stat(req.File) {
|
||||||
return buserr.WithName("ErrFileNotFound", req.File)
|
return buserr.WithName("ErrFileNotFound", req.File)
|
||||||
}
|
}
|
||||||
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
||||||
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -200,8 +198,20 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
||||||
|
// Validate AI Gateway backups before stopping the app, creating rollback
|
||||||
|
// backups, or touching its files/database. Keep the validated extraction for
|
||||||
|
// the restore so uploaded/encrypted archives are not unpacked twice.
|
||||||
|
preparedPath, cleanup, err := prepareAIGatewayAppRecover(install, recoverFile, secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if parentTask == nil && !isRollback {
|
||||||
|
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
recoverTask *task.Task
|
recoverTask *task.Task
|
||||||
isOk = false
|
isOk = false
|
||||||
rollbackFile string
|
rollbackFile string
|
||||||
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
if parentTask == nil {
|
if parentTask == nil {
|
||||||
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recoverApp := func(t *task.Task) error {
|
recoverApp := func(t *task.Task) error {
|
||||||
|
defer cleanup()
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
@@ -223,10 +235,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpPath := preparedPath
|
||||||
|
if tmpPath == "" {
|
||||||
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
tmpPath = strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
||||||
|
}
|
||||||
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
} else {
|
} else {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
}
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(tmpPath)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
||||||
@@ -397,6 +412,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer cleanup()
|
||||||
_ = recoverTask.Execute()
|
_ = recoverTask.Execute()
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
|
|||||||
}
|
}
|
||||||
config := targetContainer.Labels[composeConfigLabel]
|
config := targetContainer.Labels[composeConfigLabel]
|
||||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
files, err := resolveComposeFiles(workdir, config)
|
||||||
composePath = config
|
if err != nil {
|
||||||
} else {
|
return "", nil, err
|
||||||
composePath = workdir
|
|
||||||
}
|
}
|
||||||
|
composePath = strings.Join(files, ",")
|
||||||
}
|
}
|
||||||
composeFiles := normalizeComposeFiles(composePath)
|
composeFiles, err := normalizeComposeFiles(composePath)
|
||||||
if len(composeFiles) == 0 {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
return "", nil, err
|
||||||
}
|
}
|
||||||
return composePath, composeFiles, nil
|
return strings.Join(composeFiles, ","), composeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeComposeFiles(composePath string) []string {
|
func normalizeComposeFiles(composePath string) ([]string, error) {
|
||||||
items := strings.Split(composePath, ",")
|
files, err := resolveComposeFiles("", composePath)
|
||||||
result := make([]string, 0)
|
if err != nil {
|
||||||
seen := make(map[string]struct{})
|
return nil, err
|
||||||
for _, item := range items {
|
|
||||||
item = strings.TrimSpace(item)
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
stat, err := os.Stat(item)
|
for i, file := range files {
|
||||||
if err == nil && stat.IsDir() {
|
info, err := os.Stat(file)
|
||||||
item = path.Join(item, "docker-compose.yml")
|
if err == nil && info.IsDir() {
|
||||||
|
file = filepath.Join(file, "docker-compose.yml")
|
||||||
|
info, err = os.Stat(file)
|
||||||
|
files[i] = file
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(item); err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := seen[item]; ok {
|
if !info.Mode().IsRegular() {
|
||||||
continue
|
return nil, fmt.Errorf("compose path %s is not a regular file", file)
|
||||||
}
|
}
|
||||||
seen[item] = struct{}{}
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
}
|
||||||
sort.Strings(result)
|
return files, nil
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *composeBackupContext) close() {
|
func (c *composeBackupContext) close() {
|
||||||
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||||
if recoverCtx.targetDir != "" {
|
composeFiles := recoverCtx.meta.Files
|
||||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
|
||||||
|
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
|
filesByPath[filepath.Clean(item.OriginalPath)] = item
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
|
||||||
return err
|
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
|
||||||
|
file = strings.TrimSpace(file)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
file = filepath.Clean(file)
|
||||||
for _, item := range recoverCtx.meta.Files {
|
item, ok := filesByPath[file]
|
||||||
|
if !ok {
|
||||||
|
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
|
||||||
|
return fmt.Errorf("compose file %s is missing from backup metadata", file)
|
||||||
|
}
|
||||||
|
item = composeFiles[0]
|
||||||
|
}
|
||||||
|
orderedFiles = append(orderedFiles, item)
|
||||||
|
}
|
||||||
|
composeFiles = orderedFiles
|
||||||
|
}
|
||||||
|
if len(composeFiles) == 0 {
|
||||||
|
return fmt.Errorf("compose files are missing from backup metadata")
|
||||||
|
}
|
||||||
|
restored := make([]string, 0, len(composeFiles))
|
||||||
|
sourceByTarget := make(map[string]string, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
}
|
}
|
||||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
info, err := os.Stat(backupPath)
|
||||||
continue
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
|
||||||
}
|
}
|
||||||
targetName := item.FileName
|
targetName := item.FileName
|
||||||
if item.RelativePath != "" {
|
if item.RelativePath != "" {
|
||||||
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
||||||
}
|
}
|
||||||
|
sourcePath := item.OriginalPath
|
||||||
|
if sourcePath == "" {
|
||||||
|
sourcePath = backupPath
|
||||||
|
}
|
||||||
|
sourcePath = filepath.Clean(sourcePath)
|
||||||
|
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
|
||||||
|
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
|
||||||
|
}
|
||||||
|
sourceByTarget[targetPath] = sourcePath
|
||||||
|
restored = append(restored, targetPath)
|
||||||
|
}
|
||||||
|
if recoverCtx.targetDir != "" {
|
||||||
|
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, item := range composeFiles {
|
||||||
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
|
}
|
||||||
|
targetPath := restored[i]
|
||||||
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
restored = append(restored, targetPath)
|
|
||||||
}
|
}
|
||||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||||
if recoverCtx.fileOp.Stat(envPath) {
|
if recoverCtx.fileOp.Stat(envPath) {
|
||||||
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(restored) == 0 {
|
|
||||||
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
|
||||||
if !recoverCtx.fileOp.Stat(defaultPath) {
|
|
||||||
return fmt.Errorf("compose file not found in backup data")
|
|
||||||
}
|
|
||||||
restored = append(restored, defaultPath)
|
|
||||||
}
|
|
||||||
sort.Strings(restored)
|
|
||||||
recoverCtx.composePath = strings.Join(restored, ",")
|
recoverCtx.composePath = strings.Join(restored, ",")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-78
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
|||||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||||
|
|
||||||
type IContainerService interface {
|
type IContainerService interface {
|
||||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||||
List() []dto.ContainerOptions
|
List() []dto.ContainerOptions
|
||||||
ListByImage(imageName string) []dto.ContainerOptions
|
ListByImage(imageName string) []dto.ContainerOptions
|
||||||
LoadStatus() (dto.ContainerStatus, error)
|
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
|
||||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
ListNetwork() ([]dto.Options, error)
|
ListNetwork() ([]dto.Options, error)
|
||||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
|||||||
ContainerUpdate(req dto.ContainerOperate) error
|
ContainerUpdate(req dto.ContainerOperate) error
|
||||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
|
||||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||||
ContainerRename(req dto.ContainerRename) error
|
ContainerRename(req dto.ContainerRename) error
|
||||||
@@ -112,7 +112,7 @@ func NewIContainerService() IContainerService {
|
|||||||
return &ContainerService{}
|
return &ContainerService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
@@ -123,7 +123,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
|||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", req.Filters)
|
options.Filters.Add("label", req.Filters)
|
||||||
}
|
}
|
||||||
containers, err := client.ContainerList(context.Background(), options)
|
containers, err := client.ContainerList(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -206,27 +206,30 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
|
||||||
var data dto.ContainerStatus
|
var data dto.ContainerStatus
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
c := context.Background()
|
if !containersOnly {
|
||||||
|
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
|
||||||
data.ImageCount = len(images)
|
data.ImageCount = len(images)
|
||||||
repo, _ := imageRepoRepo.List()
|
repo, _ := imageRepoRepo.List()
|
||||||
data.RepoCount = len(repo)
|
data.RepoCount = len(repo)
|
||||||
templates, _ := composeRepo.List()
|
templates, _ := composeRepo.List()
|
||||||
data.ComposeTemplateCount = len(templates)
|
data.ComposeTemplateCount = len(templates)
|
||||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||||
data.NetworkCount = len(networks)
|
data.NetworkCount = len(networks)
|
||||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||||
data.VolumeCount = len(volumes.Volumes)
|
data.VolumeCount = len(volumes.Volumes)
|
||||||
data.ComposeCount = loadComposeCount(client)
|
data.ComposeCount = loadComposeCount(client)
|
||||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
}
|
||||||
|
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
data.ContainerCount = len(containers)
|
data.ContainerCount = len(containers)
|
||||||
for _, item := range containers {
|
for _, item := range containers {
|
||||||
switch item.State {
|
switch item.State {
|
||||||
@@ -292,25 +295,48 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
|||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
|
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
options := container.ListOptions{All: true}
|
||||||
|
if ids != nil {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []dto.ContainerListStats{}, nil
|
||||||
|
}
|
||||||
|
options.Filters = filters.NewArgs()
|
||||||
|
for _, id := range ids {
|
||||||
|
options.Filters.Add("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list, err := client.ContainerList(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
datas := make([]dto.ContainerListStats, len(list))
|
datas := make([]dto.ContainerListStats, len(list))
|
||||||
|
jobs := make(chan int)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(list))
|
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||||
for i := 0; i < len(list); i++ {
|
wg.Add(1)
|
||||||
go func(index int, item container.Summary) {
|
go func() {
|
||||||
datas[index] = loadCpuAndMem(client, item.ID)
|
defer wg.Done()
|
||||||
wg.Done()
|
for index := range jobs {
|
||||||
}(i, list[i])
|
datas[index].ContainerID = list[index].ID
|
||||||
|
if ctx.Err() == nil && list[index].State == "running" {
|
||||||
|
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
|
||||||
}
|
}
|
||||||
|
if len(list[index].Names) > 0 {
|
||||||
|
datas[index].ContainerName = strings.TrimPrefix(list[index].Names[0], "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for index := range list {
|
||||||
|
jobs <- index
|
||||||
|
}
|
||||||
|
close(jobs)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return datas, nil
|
return datas, nil
|
||||||
}
|
}
|
||||||
@@ -326,40 +352,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
case "container":
|
case "container":
|
||||||
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
||||||
case "compose":
|
case "compose":
|
||||||
filePath := ""
|
config, workdir := "", ""
|
||||||
cli, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
options := container.ListOptions{All: true}
|
options := container.ListOptions{All: true}
|
||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
||||||
containers, err := cli.ContainerList(context.Background(), options)
|
containers, err := client.ContainerList(context.Background(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
for _, container := range containers {
|
|
||||||
config := container.Labels[composeConfigLabel]
|
|
||||||
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
|
|
||||||
config = req.Detail
|
|
||||||
}
|
|
||||||
workdir := container.Labels[composeWorkdirLabel]
|
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
filePath = config
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
filePath = workdir
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
|
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||||
filePath = composeItem.Path
|
if err != nil {
|
||||||
}
|
|
||||||
if _, err := os.Stat(filePath); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
config = composeItem.Path
|
||||||
|
workdir = filepath.Dir(primaryComposePath(config))
|
||||||
|
} else {
|
||||||
|
config = containers[0].Labels[composeConfigLabel]
|
||||||
|
workdir = containers[0].Labels[composeWorkdirLabel]
|
||||||
|
}
|
||||||
|
composeFiles, err := resolveComposeFiles(workdir, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filePath := composeFiles[0]
|
||||||
|
if strings.TrimSpace(req.Detail) != "" {
|
||||||
|
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
|
||||||
|
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
|
||||||
|
}
|
||||||
|
filePath = selectedFiles[0]
|
||||||
|
}
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
|
||||||
|
}
|
||||||
content, err := os.ReadFile(filePath)
|
content, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -383,11 +416,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
name := ""
|
name := ""
|
||||||
switch req.PruneType {
|
switch req.PruneType {
|
||||||
case "container":
|
case "container":
|
||||||
@@ -408,6 +436,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
||||||
|
if err := t.TaskCtx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
pruneFilters := filters.NewArgs()
|
pruneFilters := filters.NewArgs()
|
||||||
if req.WithTagAll {
|
if req.WithTagAll {
|
||||||
pruneFilters.Add("dangling", "false")
|
pruneFilters.Add("dangling", "false")
|
||||||
@@ -431,10 +467,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||||
case "network":
|
case "network":
|
||||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
return cleanUnusedNetworks(t, client)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "volume":
|
case "volume":
|
||||||
versions, err := client.ServerVersion(context.Background())
|
versions, err := client.ServerVersion(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -803,33 +836,33 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
||||||
messageChan := make(chan string, 1024)
|
messageChan := make(chan string, 1024)
|
||||||
errorChan := make(chan error, 1)
|
errorChan := make(chan error, 1)
|
||||||
doneChan := make(chan struct{})
|
logCtx, cancel := context.WithCancel(ctx.Request.Context())
|
||||||
|
defer cancel()
|
||||||
go func() {
|
doneChan := logCtx.Done()
|
||||||
<-ctx.Request.Context().Done()
|
|
||||||
close(doneChan)
|
|
||||||
}()
|
|
||||||
go collectLogs(doneChan, params, messageChan, errorChan)
|
go collectLogs(doneChan, params, messageChan, errorChan)
|
||||||
ctx.Stream(func(w io.Writer) bool {
|
ctx.Stream(func(w io.Writer) bool {
|
||||||
select {
|
return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
|
||||||
case msg, ok := <-messageChan:
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := fmt.Fprintf(w, "data: %s\n\n", msg)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case err := <-errorChan:
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan string, errors <-chan error) bool {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return false
|
||||||
|
case message, ok := <-messages:
|
||||||
|
if ok {
|
||||||
|
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
// The collector closes both channels when it finishes. Drain all buffered
|
||||||
|
// logs before handling its result, or a ready error channel can drop the tail.
|
||||||
|
if err := <-errors; err != nil {
|
||||||
|
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
||||||
defer close(messageChan)
|
defer close(messageChan)
|
||||||
defer close(errorChan)
|
defer close(errorChan)
|
||||||
@@ -1715,11 +1748,13 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
data := dto.ContainerListStats{
|
data := dto.ContainerListStats{
|
||||||
ContainerID: containerItem,
|
ContainerID: containerItem,
|
||||||
}
|
}
|
||||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
ContainerCount: 0,
|
ContainerCount: 0,
|
||||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||||
ConfigFile: localItem.Path,
|
ConfigFile: localItem.Path,
|
||||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
|
||||||
}
|
}
|
||||||
composeItemLocal.CreatedBy = "1Panel"
|
composeItemLocal.CreatedBy = "1Panel"
|
||||||
composeItemLocal.Path = localItem.Path
|
|
||||||
composeLocalMap[localItem.Name] = composeItemLocal
|
composeLocalMap[localItem.Name] = composeItemLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
if ok {
|
if ok {
|
||||||
composeItem.CreatedBy = createdBy
|
composeItem.CreatedBy = createdBy
|
||||||
}
|
}
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
composeItem.Path = config
|
|
||||||
} else {
|
|
||||||
composeItem.Path = workdir
|
|
||||||
}
|
|
||||||
for i := 0; i < len(composeCreatedByLocal); i++ {
|
for i := 0; i < len(composeCreatedByLocal); i++ {
|
||||||
if composeCreatedByLocal[i].Name == name {
|
if composeCreatedByLocal[i].Name == name {
|
||||||
composeItem.CreatedBy = "1Panel"
|
composeItem.CreatedBy = "1Panel"
|
||||||
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
|
|
||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
|
||||||
|
value.Path = strings.Join(composeFiles, ",")
|
||||||
|
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
|
||||||
value.IsPinned = pinnedByName[key]
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return int64(total), listItem, nil
|
return int64(total), listItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeFileExists(workdir, configFile string) bool {
|
|
||||||
workdir = strings.TrimSpace(workdir)
|
|
||||||
configFile = strings.TrimSpace(configFile)
|
|
||||||
if configFile == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range strings.Split(configFile, ",") {
|
|
||||||
file := strings.TrimSpace(item)
|
|
||||||
if file == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(file) && workdir != "" {
|
|
||||||
file = filepath.Join(workdir, file)
|
|
||||||
}
|
|
||||||
file = filepath.Clean(file)
|
|
||||||
info, err := os.Stat(file)
|
|
||||||
if err == nil && !info.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if err := validateComposeCreateName(req); err != nil {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cmd.CheckIllegal(req.Path, req.Operation) {
|
if cmd.CheckIllegal(req.Path, req.Operation) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
}
|
}
|
||||||
if req.WithFile {
|
if req.WithFile {
|
||||||
for _, item := range strings.Split(req.Path, ",") {
|
for _, item := range strings.Split(req.Path, ",") {
|
||||||
if len(item) != 0 {
|
item = strings.TrimSpace(item)
|
||||||
_ = os.RemoveAll(path.Dir(item))
|
if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := os.Stat(item)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
}
|
||||||
return nil
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
||||||
if err := newComposeEnv(req.DetailPath, req.Env); err != nil {
|
if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
composeFiles, err := resolveComposeFiles("", composePath)
|
||||||
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
filesAvailable := err == nil
|
||||||
return errors.New(stdout)
|
for _, file := range composeFiles {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
filesAvailable = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filesAvailable {
|
||||||
|
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
|
||||||
|
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var removeErrors []error
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
|
||||||
|
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
return errors.Join(removeErrors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path, projectName string) error {
|
func recreateCompose(content, composePath, detailPath, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path, projectName); err != nil {
|
if stdout, err := compose.Up(composePath, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
|
|
||||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||||
for i := 0; i < len(list); i++ {
|
for i := 0; i < len(list); i++ {
|
||||||
|
if list[i].Path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
tmpPath := list[i].Path
|
tmpPath := list[i].Path
|
||||||
if strings.Contains(list[i].Path, ",") {
|
if strings.Contains(list[i].Path, ",") {
|
||||||
tmpPath = strings.Split(list[i].Path, ",")[0]
|
tmpPath = strings.Split(list[i].Path, ",")[0]
|
||||||
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
|
||||||
|
workdir = strings.TrimSpace(workdir)
|
||||||
|
var files []string
|
||||||
|
for _, item := range strings.Split(configFile, ",") {
|
||||||
|
file := strings.TrimSpace(item)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(file) {
|
||||||
|
if !filepath.IsAbs(workdir) {
|
||||||
|
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
|
||||||
|
}
|
||||||
|
file = filepath.Join(workdir, file)
|
||||||
|
}
|
||||||
|
files = append(files, filepath.Clean(file))
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, fmt.Errorf("compose config file is not specified")
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeFileExists(files []string) bool {
|
||||||
|
for _, file := range files {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var networkCleanupSlot = make(chan struct{}, 1)
|
||||||
|
|
||||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
|
||||||
|
ctx := t.TaskCtx
|
||||||
|
select {
|
||||||
|
case networkCleanupSlot <- struct{}{}:
|
||||||
|
defer func() { <-networkCleanupSlot }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deleted, skipped, failed := 0, 0, 0
|
||||||
|
defer func() {
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
|
||||||
|
"deleted": deleted, "skipped": skipped, "failed": failed,
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
var used map[string]bool
|
||||||
|
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
|
||||||
|
for _, n := range networks {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch n.Name {
|
||||||
|
case "none", "host", "bridge", "1panel-network":
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := map[string]interface{}{"name": n.Name, "id": n.ID}
|
||||||
|
if used == nil {
|
||||||
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
used = make(map[string]bool)
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.NetworkSettings == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for name, endpoint := range c.NetworkSettings.Networks {
|
||||||
|
used[name] = true
|
||||||
|
if endpoint != nil {
|
||||||
|
used[endpoint.NetworkID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if used[n.Name] || used[n.ID] {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(inspected.Containers) > 0 {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errdefs.IsNotFound(err):
|
||||||
|
skipped++
|
||||||
|
case errdefs.IsConflict(err):
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
default:
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if failed > 0 {
|
||||||
|
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
|||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
}
|
}
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
if alertInfo.SendCount != 0 {
|
if alertInfo.SendCount != 0 {
|
||||||
item.AlertCount = alertInfo.SendCount
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
|||||||
AlertType: cronjob.Type,
|
AlertType: cronjob.Type,
|
||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
}
|
}
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
|
||||||
item.AlertMethod = alertInfo.Method
|
item.AlertMethod = alertInfo.Method
|
||||||
if alertInfo.SendCount != 0 {
|
item.AlertTitle = alertInfo.Title
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
|
if alertInfo.Status == constant.AlertEnable {
|
||||||
item.AlertCount = alertInfo.SendCount
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
item.AlertCount = 0
|
item.AlertCount = 0
|
||||||
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
|
||||||
if alertInfo.SendCount != 0 {
|
|
||||||
item.AlertCount = alertInfo.SendCount
|
|
||||||
item.AlertTitle = alertInfo.Title
|
item.AlertTitle = alertInfo.Title
|
||||||
item.AlertMethod = alertInfo.Method
|
item.AlertMethod = alertInfo.Method
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
|
if alertInfo.Status == constant.AlertEnable {
|
||||||
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
item.AlertCount = 0
|
item.AlertCount = 0
|
||||||
}
|
}
|
||||||
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||||
|
for _, item := range req {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if item.AlertCount != 0 {
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, item := range req {
|
for _, item := range req {
|
||||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||||
if cronjobItem.ID != 0 {
|
if cronjobItem.ID != 0 {
|
||||||
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
|||||||
} else {
|
} else {
|
||||||
cronjob.Status = constant.StatusDisable
|
cronjob.Status = constant.StatusDisable
|
||||||
}
|
}
|
||||||
_ = cronjobRepo.Create(&cronjob)
|
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||||
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
|
return err
|
||||||
|
}
|
||||||
|
if item.AlertTitle != "" && item.AlertMethod != "" {
|
||||||
|
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||||
|
status := constant.AlertEnable
|
||||||
|
if item.AlertCount == 0 {
|
||||||
|
status = constant.AlertDisable
|
||||||
|
}
|
||||||
createAlert := dto.AlertCreate{
|
createAlert := dto.AlertCreate{
|
||||||
Title: item.AlertTitle,
|
Title: item.AlertTitle,
|
||||||
SendCount: item.AlertCount,
|
SendCount: item.AlertCount,
|
||||||
Method: item.AlertMethod,
|
Method: item.AlertMethod,
|
||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronjob.ID)),
|
Project: strconv.Itoa(int(cronjob.ID)),
|
||||||
Status: constant.AlertEnable,
|
Status: status,
|
||||||
|
AdvancedParams: advanced,
|
||||||
|
}
|
||||||
|
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
_ = NewIAlertService().CreateAlert(createAlert, operator)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.AlertCount != 0 {
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
||||||
if cronjob.ID != 0 {
|
if cronjob.ID != 0 {
|
||||||
return buserr.New("ErrRecordExist")
|
return buserr.New("ErrRecordExist")
|
||||||
@@ -609,6 +644,7 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
|||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronjob.ID)),
|
Project: strconv.Itoa(int(cronjob.ID)),
|
||||||
Status: constant.AlertEnable,
|
Status: constant.AlertEnable,
|
||||||
|
AdvancedParams: advanced,
|
||||||
}
|
}
|
||||||
err := NewIAlertService().CreateAlert(createAlert, operator)
|
err := NewIAlertService().CreateAlert(createAlert, operator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var cronjob model.Cronjob
|
var cronjob model.Cronjob
|
||||||
if err := copier.Copy(&cronjob, &req); err != nil {
|
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
}
|
}
|
||||||
|
if req.AlertCount != 0 {
|
||||||
|
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
|
||||||
|
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
method := req.AlertMethod
|
||||||
|
if method == "" {
|
||||||
|
method = previous.Method
|
||||||
|
}
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
cronjob.EntryIDs = cronModel.EntryIDs
|
cronjob.EntryIDs = cronModel.EntryIDs
|
||||||
cronjob.Type = cronModel.Type
|
cronjob.Type = cronModel.Type
|
||||||
@@ -758,6 +812,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
Method: req.AlertMethod,
|
Method: req.AlertMethod,
|
||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronModel.ID)),
|
Project: strconv.Itoa(int(cronModel.ID)),
|
||||||
|
AdvancedParams: advanced,
|
||||||
}
|
}
|
||||||
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cronJobAlertAdvancedParams(mode string) (string, error) {
|
||||||
|
if mode == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams("", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||||
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
||||||
if cronjob.ID == 0 {
|
if cronjob.ID == 0 {
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
|
|||||||
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
|
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,6 +24,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||||
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
|||||||
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||||
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||||
}()
|
}()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
|||||||
record.TaskID = ""
|
record.TaskID = ""
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||||
if len(taskItem.ID) == 0 {
|
if len(storedTask.ID) == 0 {
|
||||||
record.TaskID = ""
|
record.TaskID = ""
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
} else {
|
} else {
|
||||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||||
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
|
|||||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
const cronJobSkippedStep = "cronjob-skipped"
|
||||||
|
|
||||||
|
func cronJobAlertResult(taskItem *task.Task, err error) string {
|
||||||
|
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
|
||||||
|
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return alertUtil.CronJobAlertFailed
|
||||||
|
}
|
||||||
|
if taskItem.Task.Status != constant.StatusSuccess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, subTask := range taskItem.SubTasks {
|
||||||
|
if subTask.StepAlias != cronJobSkippedStep {
|
||||||
|
return alertUtil.CronJobAlertSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
|
||||||
|
if result == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
pushAlert := dto.PushAlert{
|
pushAlert := dto.PushAlert{
|
||||||
|
Result: result,
|
||||||
TaskName: cronjob.Name,
|
TaskName: cronjob.Name,
|
||||||
AlertType: cronjob.Type,
|
AlertType: cronjob.Type,
|
||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
|
|||||||
+102
-60
@@ -43,6 +43,10 @@ type IDashboardService interface {
|
|||||||
|
|
||||||
LoadQuickOptions() []dto.QuickJump
|
LoadQuickOptions() []dto.QuickJump
|
||||||
ChangeQuick(req dto.ChangeQuicks) error
|
ChangeQuick(req dto.ChangeQuicks) error
|
||||||
|
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||||
|
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
|
||||||
|
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
|
||||||
|
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
|
||||||
|
|
||||||
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
||||||
ChangeShow(req dto.SettingUpdate) error
|
ChangeShow(req dto.SettingUpdate) error
|
||||||
@@ -242,7 +246,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||||
|
|
||||||
currentInfo.DiskData = loadDiskInfo()
|
currentInfo.DiskData = loadDiskInfo(false)
|
||||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||||
|
|
||||||
if ioOption == "all" {
|
if ioOption == "all" {
|
||||||
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
|||||||
}
|
}
|
||||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||||
showCount := 0
|
showCount := 0
|
||||||
var quicks []model.QuickJump
|
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||||
for _, item := range req.Quicks {
|
for _, item := range req.Quicks {
|
||||||
var quick model.QuickJump
|
|
||||||
if item.IsShow {
|
if item.IsShow {
|
||||||
showCount++
|
showCount++
|
||||||
}
|
}
|
||||||
if err := copier.Copy(&quick, &item); err != nil {
|
quicks = append(quicks, model.QuickJump{
|
||||||
return err
|
BaseModel: model.BaseModel{ID: item.ID},
|
||||||
}
|
IsShow: item.IsShow,
|
||||||
quicks = append(quicks, quick)
|
})
|
||||||
}
|
}
|
||||||
if showCount == 0 {
|
if showCount == 0 {
|
||||||
return buserr.New("ErrMinQuickJump")
|
return buserr.New("ErrMinQuickJump")
|
||||||
@@ -455,26 +458,9 @@ type diskInfo struct {
|
|||||||
Device string
|
Device string
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDiskInfo() []dto.DiskInfo {
|
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||||
var datas []dto.DiskInfo
|
var datas []dto.DiskInfo
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
stdout := loadDiskMounts()
|
||||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
|
||||||
stdout, err := cmdMgr.RunPipe(
|
|
||||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
|
||||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
|
||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
|
||||||
stdout, err = cmdMgr2.RunPipe(
|
|
||||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
|
||||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
|
||||||
return datas
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines := strings.Split(stdout, "\n")
|
lines := strings.Split(stdout, "\n")
|
||||||
|
|
||||||
var mounts []diskInfo
|
var mounts []diskInfo
|
||||||
@@ -522,43 +508,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
itemData.Type = mount.Type
|
itemData.Type = mount.Type
|
||||||
itemData.Device = mount.Device
|
itemData.Device = mount.Device
|
||||||
|
|
||||||
type diskResult struct {
|
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||||
state *disk.UsageStat
|
if err != nil {
|
||||||
err error
|
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||||
|
} else {
|
||||||
|
itemData.Total = state.Total
|
||||||
|
itemData.Free = state.Free
|
||||||
|
itemData.Used = state.Used
|
||||||
|
itemData.UsedPercent = state.UsedPercent
|
||||||
|
itemData.InodesTotal = state.InodesTotal
|
||||||
|
itemData.InodesUsed = state.InodesUsed
|
||||||
|
itemData.InodesFree = state.InodesFree
|
||||||
|
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||||
}
|
}
|
||||||
resultCh := make(chan diskResult, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
|
||||||
resultCh <- diskResult{state: state, err: err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
datas = append(datas, itemData)
|
datas = append(datas, itemData)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
|
||||||
case result := <-resultCh:
|
|
||||||
if result.err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
datas = append(datas, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemData.Total = result.state.Total
|
|
||||||
itemData.Free = result.state.Free
|
|
||||||
itemData.Used = result.state.Used
|
|
||||||
itemData.UsedPercent = result.state.UsedPercent
|
|
||||||
itemData.InodesTotal = result.state.InodesTotal
|
|
||||||
itemData.InodesUsed = result.state.InodesUsed
|
|
||||||
itemData.InodesFree = result.state.InodesFree
|
|
||||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
|
||||||
mu.Lock()
|
|
||||||
datas = append(datas, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(mounts[i])
|
}(mounts[i])
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -569,6 +534,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var diskMountsMu sync.Mutex
|
||||||
|
|
||||||
|
func loadDiskMounts() string {
|
||||||
|
if !diskMountsMu.TryLock() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
resultCh := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
var stdout string
|
||||||
|
defer func() {
|
||||||
|
diskMountsMu.Unlock()
|
||||||
|
resultCh <- stdout
|
||||||
|
}()
|
||||||
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||||
|
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||||
|
output, err := cmdMgr.RunPipe(
|
||||||
|
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||||
|
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||||
|
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||||
|
output, err = cmdMgr2.RunPipe(
|
||||||
|
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||||
|
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout = output
|
||||||
|
}()
|
||||||
|
timer := time.NewTimer(3 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case stdout := <-resultCh:
|
||||||
|
return stdout
|
||||||
|
case <-timer.C:
|
||||||
|
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||||
|
type diskResult struct {
|
||||||
|
state *disk.UsageStat
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan diskResult, 1)
|
||||||
|
go func() {
|
||||||
|
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||||
|
resultCh <- diskResult{state: state, err: err}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||||
|
case result := <-resultCh:
|
||||||
|
return result.state, result.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||||
ok, client := accelerator.New()
|
ok, client := accelerator.New()
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -588,6 +616,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
|||||||
xpuData []dto.XPUInfo
|
xpuData []dto.XPUInfo
|
||||||
)
|
)
|
||||||
for _, device := range snapshot.Devices {
|
for _, device := range snapshot.Devices {
|
||||||
|
if device.ParentID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
switch device.Kind {
|
switch device.Kind {
|
||||||
case accelerator.KindGPU:
|
case accelerator.KindGPU:
|
||||||
if device.GPU == nil {
|
if device.GPU == nil {
|
||||||
@@ -597,7 +628,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
|||||||
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
dataItem.MaxPowerLimit = device.GPU.PowerLimit
|
||||||
|
dataItem.PowerUsage = dataItem.PowerDraw
|
||||||
|
if dataItem.MaxPowerLimit != "" {
|
||||||
|
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
|
||||||
|
}
|
||||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||||
gpuData = append(gpuData, dataItem)
|
gpuData = append(gpuData, dataItem)
|
||||||
case accelerator.KindNPU:
|
case accelerator.KindNPU:
|
||||||
@@ -674,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
|
|||||||
quicks := launcherRepo.ListQuickJump(false)
|
quicks := launcherRepo.ListQuickJump(false)
|
||||||
for i := 0; i < len(quicks); i++ {
|
for i := 0; i < len(quicks); i++ {
|
||||||
switch quicks[i].Name {
|
switch quicks[i].Name {
|
||||||
|
case "File":
|
||||||
|
count, err := launcherRepo.CountQuickDirectories()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load quick directory count: %v", err)
|
||||||
|
}
|
||||||
|
quicks[i].Detail = fmt.Sprintf("%d", count)
|
||||||
|
quicks[i].Alias = ""
|
||||||
case "Agent":
|
case "Agent":
|
||||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||||
case "Website":
|
case "Website":
|
||||||
|
|||||||
@@ -433,6 +433,10 @@ func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||||
|
return u.create(ctx, req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBounded bool) (*model.DatabaseMysql, error) {
|
||||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
||||||
return nil, buserr.New("ErrCmdIllegal")
|
return nil, buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -470,13 +474,13 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return nil, errors.New("cannot set root as user name")
|
return nil, errors.New("cannot set root as user name")
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
cli, version, err := loadMysqlClientByFrom(req.Database, mcpBounded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
createItem.MysqlName = req.Database
|
createItem.MysqlName = req.Database
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
if err := cli.Create(client.CreateInfo{
|
createInfo := client.CreateInfo{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Format: req.Format,
|
Format: req.Format,
|
||||||
Collation: req.Collation,
|
Collation: req.Collation,
|
||||||
@@ -485,9 +489,31 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
Permission: req.Permission,
|
Permission: req.Permission,
|
||||||
Version: version,
|
Version: version,
|
||||||
Timeout: 300,
|
Timeout: 300,
|
||||||
}); err != nil {
|
}
|
||||||
|
if mcpBounded {
|
||||||
|
local, ok := cli.(*client.Local)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("an installed local MySQL instance is required")
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(300 * time.Second)
|
||||||
|
if writeDeadline, ok := ctx.Deadline(); ok && writeDeadline.Add(-10*time.Second).Before(deadline) {
|
||||||
|
deadline = writeDeadline.Add(-10 * time.Second)
|
||||||
|
}
|
||||||
|
sqlContext, cancel := context.WithDeadline(ctx, deadline)
|
||||||
|
err = local.CreateWithContext(sqlContext, createInfo)
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
err = cli.Create(createInfo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if mcpBounded {
|
||||||
|
if err := persistMCPMysqlMetadata(ctx, req, permissionHosts, &createItem); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &createItem, nil
|
||||||
|
}
|
||||||
if len(req.Username) != 0 {
|
if len(req.Username) != 0 {
|
||||||
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -506,6 +532,45 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return &createItem, nil
|
return &createItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func persistMCPMysqlMetadata(ctx context.Context, req dto.MysqlDBCreate, hosts []string, item *model.DatabaseMysql) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if req.Username != "" {
|
||||||
|
key := global.CONF.Base.EncryptKey
|
||||||
|
if key == "" {
|
||||||
|
var setting model.Setting
|
||||||
|
if err := tx.Where("key = ?", "EncryptKey").First(&setting).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key = setting.Value
|
||||||
|
}
|
||||||
|
password, err := encrypt.StringEncryptWithKey(req.Password, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, host := range hosts {
|
||||||
|
var user model.DatabaseUser
|
||||||
|
err := tx.Where("type = ? AND database = ? AND username = ? AND host = ?", "mysql", req.Database, req.Username, host).First(&user).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Type, user.Database, user.Username, user.Host = "mysql", req.Database, req.Username, host
|
||||||
|
user.Password, user.IsDelete = password, false
|
||||||
|
if req.Description != "" {
|
||||||
|
user.Description = req.Description
|
||||||
|
}
|
||||||
|
if err := tx.Save(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
grant := model.DatabaseUserGrant{Type: "mysql", Database: req.Database, DBName: req.Name, Username: req.Username, Host: host}
|
||||||
|
if err := tx.Where("type = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", grant.Type, grant.Database, grant.DBName, grant.Username, grant.Host).FirstOrCreate(&grant).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Create(item).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
||||||
dbType, err := resolveDatabaseUserType(req.Database)
|
dbType, err := resolveDatabaseUserType(req.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1398,7 +1463,14 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;")
|
return loadMysqlStatus(req, app, executeSqlForMaps, executeSqlForRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMysqlStatus(req dto.OperationWithNameAndType, app *repo.RootInfo,
|
||||||
|
maps func(string, string, string, string) (map[string]string, error),
|
||||||
|
rowsQuery func(string, string, string, string) ([]string, error),
|
||||||
|
) (*dto.MysqlStatus, error) {
|
||||||
|
statusMap, err := maps(app.ContainerName, app.Key, app.Password, "show global status;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1498,7 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
|||||||
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
||||||
masterStatus = "show binary log status;"
|
masterStatus = "show binary log status;"
|
||||||
}
|
}
|
||||||
rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
|
rows, err := rowsQuery(app.ContainerName, app.Key, app.Password, masterStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1531,6 +1603,10 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
||||||
|
return loadMysqlClientByFrom(database, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMysqlClientByFrom(database string, mcpLocalOnly bool) (mysql.MysqlClient, string, error) {
|
||||||
var (
|
var (
|
||||||
dbInfo client.DBInfo
|
dbInfo client.DBInfo
|
||||||
version string
|
version string
|
||||||
@@ -1542,6 +1618,9 @@ func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
if mcpLocalOnly && (databaseItem.ID == 0 || databaseItem.From != "local" || databaseItem.Type != "mysql" || databaseItem.AppInstallID == 0) {
|
||||||
|
return nil, "", errors.New("an installed local MySQL instance is required")
|
||||||
|
}
|
||||||
dbInfo.Type = databaseItem.Type
|
dbInfo.Type = databaseItem.Type
|
||||||
dbInfo.From = databaseItem.From
|
dbInfo.From = databaseItem.From
|
||||||
dbInfo.Database = database
|
dbInfo.Database = database
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *MysqlService) CreateForMCP(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||||
|
if req.From != "local" || ctx.Err() != nil {
|
||||||
|
return nil, errors.New("invalid MCP database target or request")
|
||||||
|
}
|
||||||
|
writeContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), 310*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
created, err := u.create(writeContext, req, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mcpapi.SanitizeBusinessError(err, "database creation failed; inspect the invocation before retrying")
|
||||||
|
}
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -17,12 +23,15 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"github.com/docker/docker/api/types/container"
|
dockerclient "github.com/docker/docker/client"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisService struct{}
|
type RedisService struct{}
|
||||||
|
|
||||||
|
// The CLI container is shared by all remote Redis databases on this node.
|
||||||
|
var redisCliInstallMutex sync.Mutex
|
||||||
|
|
||||||
type IRedisService interface {
|
type IRedisService interface {
|
||||||
UpdateConf(req dto.RedisConfUpdate) error
|
UpdateConf(req dto.RedisConfUpdate) error
|
||||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
|||||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||||
|
|
||||||
CheckHasCli() bool
|
CheckHasCli() bool
|
||||||
InstallCli() error
|
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRedisService() IRedisService {
|
func NewIRedisService() IRedisService {
|
||||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) CheckHasCli() bool {
|
func (u *RedisService) CheckHasCli() bool {
|
||||||
client, err := docker.NewDockerClient()
|
installed, _ := u.checkCliInstalled()
|
||||||
if err != nil {
|
return installed
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range containerLists {
|
|
||||||
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) InstallCli() error {
|
func (u *RedisService) checkCliInstalled() (bool, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
|
||||||
|
if !redisCliInstallMutex.TryLock() {
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer redisCliInstallMutex.Unlock()
|
||||||
item := dto.ContainerOperate{
|
item := dto.ContainerOperate{
|
||||||
|
TaskID: req.TaskID,
|
||||||
Name: "1Panel-redis-cli-tools",
|
Name: "1Panel-redis-cli-tools",
|
||||||
Image: "redis:7.4.4",
|
Image: "redis:7.4.4",
|
||||||
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
||||||
}
|
}
|
||||||
return NewIContainerService().ContainerCreate(item, false)
|
running, err := taskRepo.GetFirst(
|
||||||
|
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
|
||||||
|
repo.WithByType(task.TaskScopeContainer),
|
||||||
|
taskRepo.WithByStatus(constant.StatusExecuting),
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if running.ID != "" {
|
||||||
|
return running.ID, nil
|
||||||
|
}
|
||||||
|
installed, err := u.checkCliInstalled()
|
||||||
|
if err != nil || installed {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if item.TaskID == "" {
|
||||||
|
item.TaskID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return item.TaskID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
|||||||
if baseInfo.SwapMemoryTotal != 0 {
|
if baseInfo.SwapMemoryTotal != 0 {
|
||||||
baseInfo.SwapDetails = loadSwap()
|
baseInfo.SwapDetails = loadSwap()
|
||||||
}
|
}
|
||||||
disks := loadDiskInfo()
|
disks := loadDiskInfo(false)
|
||||||
for _, item := range disks {
|
for _, item := range disks {
|
||||||
baseInfo.MaxSize += item.Free
|
baseInfo.MaxSize += item.Free
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -487,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
|||||||
if err := controller.Handle(req.Operation, service); err != nil {
|
if err := controller.Handle(req.Operation, service); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Operation == "start" || req.Operation == "restart" {
|
||||||
|
return RestoreDockerPortGuard(context.Background())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ var (
|
|||||||
ftpRepo = repo.NewIFtpRepo()
|
ftpRepo = repo.NewIFtpRepo()
|
||||||
clamRepo = repo.NewIClamRepo()
|
clamRepo = repo.NewIClamRepo()
|
||||||
monitorRepo = repo.NewIMonitorRepo()
|
monitorRepo = repo.NewIMonitorRepo()
|
||||||
|
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||||
|
|
||||||
settingRepo = repo.NewISettingRepo()
|
settingRepo = repo.NewISettingRepo()
|
||||||
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
|
|
||||||
backupRepo = repo.NewIBackupRepo()
|
backupRepo = repo.NewIBackupRepo()
|
||||||
|
|
||||||
websiteRepo = repo.NewIWebsiteRepo()
|
websiteRepo = repo.NewIWebsiteRepo()
|
||||||
|
|||||||
@@ -882,6 +882,16 @@ func (f *FileService) ChangeName(req request.FileRename) error {
|
|||||||
if hasInvalidFileName(req.NewName) {
|
if hasInvalidFileName(req.NewName) {
|
||||||
return buserr.New("ErrInvalidChar")
|
return buserr.New("ErrInvalidChar")
|
||||||
}
|
}
|
||||||
|
if req.OldName == req.NewName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// os.Rename silently replaces an existing target on Linux, so reject the
|
||||||
|
// rename here to avoid losing the target file without confirmation.
|
||||||
|
if _, err := os.Lstat(req.NewName); err == nil {
|
||||||
|
return buserr.New("ErrFileIsExist")
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fo := files.NewFileOp()
|
fo := files.NewFileOp()
|
||||||
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
||||||
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
||||||
@@ -1310,7 +1320,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||||
return loadDiskInfo()
|
return loadDiskInfo(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChangeNameRejectsExistingTarget(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
oldPath := filepath.Join(dir, "old.txt")
|
||||||
|
targetPath := filepath.Join(dir, "target.txt")
|
||||||
|
// Binary source content keeps this test independent of the file-history DB.
|
||||||
|
if err := os.WriteFile(oldPath, []byte("old\x00"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(targetPath, []byte("target"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: targetPath})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error when the target name already exists")
|
||||||
|
}
|
||||||
|
var bizErr buserr.BusinessError
|
||||||
|
if !errors.As(err, &bizErr) || bizErr.Msg != "ErrFileIsExist" {
|
||||||
|
t.Fatalf("expected ErrFileIsExist, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(content) != "target" {
|
||||||
|
t.Fatalf("target file was overwritten, content = %q", content)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(oldPath); err != nil {
|
||||||
|
t.Fatalf("source file should still exist after a rejected rename: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangeNameRenamesToNewName(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
oldPath := filepath.Join(dir, "old")
|
||||||
|
newPath := filepath.Join(dir, "new")
|
||||||
|
if err := os.Mkdir(oldPath, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: newPath}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(newPath); err != nil {
|
||||||
|
t.Fatalf("renamed path does not exist: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("old path should be gone, stat err = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+475
-881
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
@@ -39,9 +38,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DockerPortGuardService struct {
|
type DockerPortGuardService struct {
|
||||||
policies repo.IDockerPortGuardRepo
|
|
||||||
runtime dockerfirewall.Runtime
|
runtime dockerfirewall.Runtime
|
||||||
runtimeForBackend func(string) dockerfirewall.Runtime
|
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
|
||||||
client func() (*client.Client, error)
|
client func() (*client.Client, error)
|
||||||
version func(string) string
|
version func(string) string
|
||||||
}
|
}
|
||||||
@@ -50,24 +48,35 @@ var dockerPortGuardServiceMu sync.Mutex
|
|||||||
|
|
||||||
type IDockerPortGuardService interface {
|
type IDockerPortGuardService interface {
|
||||||
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
||||||
|
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||||
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
||||||
Operate(context.Context, dto.DockerPortGuardOperation) error
|
Operate(context.Context, dto.DockerPortGuardOperation) error
|
||||||
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
||||||
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
||||||
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
||||||
Reconcile(context.Context) error
|
Restore(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDockerPortGuardService() IDockerPortGuardService {
|
||||||
|
return newDockerPortGuardService()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
||||||
policies, err := s.policies.ListManaged(ctx)
|
families, err := loadFirewallFamilies()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.DockerPortGuardList{}, err
|
return dto.DockerPortGuardList{}, err
|
||||||
}
|
}
|
||||||
|
backend := selectedDockerFirewallBackend("")
|
||||||
|
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return dto.DockerPortGuardList{}, err
|
||||||
|
}
|
||||||
|
policies := dockerGuardInventoryEndpoints(inventory)
|
||||||
unavailable := func() dto.DockerPortGuardList {
|
unavailable := func() dto.DockerPortGuardList {
|
||||||
backend := selectedDockerFirewallBackend("")
|
backend := selectedDockerFirewallBackend("")
|
||||||
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||||
base.Message = i18n.Get("ErrDockerFailed")
|
base.Message = i18n.Get("ErrDockerFailed")
|
||||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}
|
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
|
||||||
}
|
}
|
||||||
cli, err := s.client()
|
cli, err := s.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,13 +88,13 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
|
|||||||
return unavailable(), nil
|
return unavailable(), nil
|
||||||
}
|
}
|
||||||
detectedBackend := dockerFirewallBackend(info)
|
detectedBackend := dockerFirewallBackend(info)
|
||||||
backend := selectedDockerFirewallBackend(detectedBackend)
|
backend = selectedDockerFirewallBackend(detectedBackend)
|
||||||
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.DockerPortGuardList{}, err
|
return dto.DockerPortGuardList{}, err
|
||||||
}
|
}
|
||||||
annotateDockerEndpointManagement(endpoints, detectedBackend)
|
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
|
||||||
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
||||||
sort.Slice(endpoints, func(i, j int) bool {
|
sort.Slice(endpoints, func(i, j int) bool {
|
||||||
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
||||||
@@ -96,6 +105,24 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
|
|||||||
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||||
|
backend := string(provider)
|
||||||
|
if backend == "" {
|
||||||
|
backend = selectedDockerFirewallBackend("")
|
||||||
|
}
|
||||||
|
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
||||||
|
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
|
||||||
|
}
|
||||||
|
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return dto.FirewallSubsystemBackup{}, err
|
||||||
|
}
|
||||||
|
if inventory.Policies == nil {
|
||||||
|
inventory.Policies = []dockerfirewall.Policy{}
|
||||||
|
}
|
||||||
|
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
||||||
cli, err := s.client()
|
cli, err := s.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,7 +144,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
|
|||||||
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
||||||
backend = dockerFirewallBackend(info)
|
backend = dockerFirewallBackend(info)
|
||||||
}
|
}
|
||||||
annotateDockerEndpointManagement(endpoints, backend)
|
annotateDockerEndpointManagement(ctx, endpoints, backend)
|
||||||
return groupDockerGuardContainers(endpoints), nil
|
return groupDockerGuardContainers(endpoints), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,34 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
|||||||
defer dockerPortGuardServiceMu.Unlock()
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
switch request.Operation {
|
switch request.Operation {
|
||||||
case "initialize":
|
case "initialize":
|
||||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
return s.initialize(ctx, request, nil)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
policies, err := s.runtimePolicies(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
inventory, err := runtime.ListPolicies()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := runtime.Initialize(policies, inventory); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "bind":
|
case "bind":
|
||||||
runtime, _, err := s.runtimeForDocker(ctx)
|
runtime, _, err := s.runtimeForDocker(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := runtime.Bind(); err != nil {
|
families, err := loadFirewallFamilies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||||
@@ -162,9 +172,9 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
|||||||
if s.runtime != nil {
|
if s.runtime != nil {
|
||||||
err = s.runtime.Unbind()
|
err = s.runtime.Unbind()
|
||||||
} else {
|
} else {
|
||||||
err = errors.Join(dockerfirewall.NewIptables().Unbind(), dockerfirewall.NewNftables().Unbind())
|
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err = errors.Join(err, ctx.Err()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||||
@@ -174,55 +184,22 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
|
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
|
||||||
if request.Operation != "initialize" {
|
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued")
|
|
||||||
}
|
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
return dto.FilterChainOperationResponse{}, err
|
return dto.FilterChainOperationResponse{}, err
|
||||||
}
|
}
|
||||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
if request.Operation != "initialize" {
|
||||||
if err != nil {
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
|
|
||||||
}
|
}
|
||||||
var runtime dockerfirewall.Runtime
|
if request.BackupFile != "" {
|
||||||
var backend string
|
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error {
|
return dto.FilterChainOperationResponse{}, err
|
||||||
var err error
|
|
||||||
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
t.Logf("backend=%s", backend)
|
}
|
||||||
return nil
|
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
|
||||||
}, nil)
|
|
||||||
taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
|
|
||||||
dockerPortGuardServiceMu.Lock()
|
dockerPortGuardServiceMu.Lock()
|
||||||
defer dockerPortGuardServiceMu.Unlock()
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
policies, err := s.runtimePolicies(t.TaskCtx)
|
return s.initialize(t.TaskCtx, request, t)
|
||||||
if err != nil {
|
})
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.Logf("backend=%s", backend)
|
|
||||||
inventory, err := runtime.ListPolicies()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return runtime.Initialize(policies, inventory)
|
|
||||||
}, nil)
|
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error {
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, nil)
|
|
||||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err)
|
|
||||||
}
|
|
||||||
go func() { _ = taskItem.Execute() }()
|
|
||||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
||||||
@@ -230,20 +207,34 @@ func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolic
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.FilterChainOperationResponse{}, err
|
return dto.FilterChainOperationResponse{}, err
|
||||||
}
|
}
|
||||||
labels := make([]string, len(uuids))
|
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
|
||||||
for i, id := range uuids {
|
ctx := t.TaskCtx
|
||||||
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
|
|
||||||
}
|
|
||||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
|
|
||||||
dockerPortGuardServiceMu.Lock()
|
dockerPortGuardServiceMu.Lock()
|
||||||
defer dockerPortGuardServiceMu.Unlock()
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
if err := ctx.Err(); err != nil {
|
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
|
inventory, err := runtime.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.reconcileLocked(ctx)
|
wanted := make(map[string]bool, len(uuids))
|
||||||
|
for _, id := range uuids {
|
||||||
|
wanted[id] = true
|
||||||
|
}
|
||||||
|
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
|
||||||
|
for _, policy := range inventory.Policies {
|
||||||
|
if wanted[policy.UUID] {
|
||||||
|
delete(wanted, policy.UUID)
|
||||||
|
} else {
|
||||||
|
remaining = append(remaining, policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(wanted) > 0 {
|
||||||
|
return filter.ErrRuleStale
|
||||||
|
}
|
||||||
|
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +243,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
|||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||||
}
|
}
|
||||||
labels := make([]string, len(request.Policies))
|
labels := make([]string, len(request.Policies))
|
||||||
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
|
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
|
||||||
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
||||||
count := 0
|
count := 0
|
||||||
for i, policy := range request.Policies {
|
for i, policy := range request.Policies {
|
||||||
@@ -264,7 +255,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
||||||
}
|
}
|
||||||
if normalized.Mode == dockerfirewall.ModeAll {
|
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
|
||||||
count++
|
count++
|
||||||
} else {
|
} else {
|
||||||
count += len(normalized.Sources)
|
count += len(normalized.Sources)
|
||||||
@@ -275,42 +266,142 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
|||||||
if count > filter.MaxAtomicExpansion {
|
if count > filter.MaxAtomicExpansion {
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||||
}
|
}
|
||||||
encoded, err := json.Marshal(normalized.Sources)
|
normalized.UUID = uuid.NewString()
|
||||||
if err != nil {
|
policies = append(policies, normalized)
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
|
||||||
}
|
}
|
||||||
policies = append(policies, model.DockerPortGuardPolicy{
|
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
|
||||||
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
|
ctx := t.TaskCtx
|
||||||
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
|
|
||||||
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
|
|
||||||
})
|
|
||||||
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
|
||||||
Family: normalized.Family, HostIP: normalized.HostIP, HostPort: normalized.HostPort, Protocol: normalized.Protocol,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error {
|
|
||||||
dockerPortGuardServiceMu.Lock()
|
dockerPortGuardServiceMu.Lock()
|
||||||
defer dockerPortGuardServiceMu.Unlock()
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inventory, err := runtime.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
|
||||||
|
if request.Import {
|
||||||
|
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
|
||||||
|
merged, err := mergeDockerBackup(inventory, backup, backend, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current = merged.Policies
|
||||||
|
}
|
||||||
|
byEndpoint := make(map[string]int, len(current))
|
||||||
|
for i, policy := range current {
|
||||||
|
byEndpoint[dockerPolicyEndpointKey(policy)] = i
|
||||||
|
}
|
||||||
|
for i := range policies {
|
||||||
|
key := dockerPolicyEndpointKey(policies[i])
|
||||||
|
index, exists := byEndpoint[key]
|
||||||
|
if request.Import {
|
||||||
|
if !exists || current[index].UUID != policies[i].UUID {
|
||||||
|
labels[i] = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if exists {
|
||||||
|
policies[i].UUID = current[index].UUID
|
||||||
|
current[index] = policies[i]
|
||||||
|
} else {
|
||||||
|
byEndpoint[key] = len(current)
|
||||||
|
current = append(current, policies[i])
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
||||||
|
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(endpoints) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
|
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.reconcileLocked(ctx)
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIDockerPortGuardService() IDockerPortGuardService {
|
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
|
||||||
return newDockerPortGuardService()
|
dockerPortGuardServiceMu.Lock()
|
||||||
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
|
enabled, err := dockerPortGuardPersistedEnabled()
|
||||||
|
if err != nil || !enabled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
families, err := loadFirewallFamilies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||||
|
before := len(backup.Docker.Policies)
|
||||||
|
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
|
||||||
|
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
|
||||||
|
}
|
||||||
|
missing := make(map[string]bool)
|
||||||
|
needsBind := false
|
||||||
|
for _, family := range families {
|
||||||
|
status := runtime.Status(family)
|
||||||
|
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
||||||
|
return fmt.Errorf("inspect Docker guard %s failed", family)
|
||||||
|
}
|
||||||
|
if status.Reason == dockerfirewall.ReasonCommandMissing {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missing[family] = !status.Initialized
|
||||||
|
needsBind = needsBind || (status.Initialized && !status.Effective)
|
||||||
|
}
|
||||||
|
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
|
||||||
|
if needsBind {
|
||||||
|
return runtime.Bind(families...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
inventory, err := runtime.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, policy := range inventory.Policies {
|
||||||
|
missing[policy.Family] = false
|
||||||
|
}
|
||||||
|
if inventory.RuleOrders == nil {
|
||||||
|
inventory.RuleOrders = make(map[string][]int64)
|
||||||
|
}
|
||||||
|
for _, policy := range backup.Docker.Policies {
|
||||||
|
if !missing[policy.Family] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inventory.Policies = append(inventory.Policies, policy)
|
||||||
|
key := policy.Family + "\x00" + policy.UUID
|
||||||
|
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
|
||||||
|
}
|
||||||
|
return runtime.Initialize(inventory.Policies, inventory, families...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string) dto.DockerPortGuardBase {
|
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
|
||||||
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
|
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
|
||||||
ipv6 := runtime.Status(dockerfirewall.FamilyIPv6)
|
var ipv6 dockerfirewall.FamilyStatus
|
||||||
|
if ipv6Enabled {
|
||||||
|
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
|
||||||
|
}
|
||||||
version := "-"
|
version := "-"
|
||||||
if s.version != nil {
|
if s.version != nil {
|
||||||
version = s.version(backend)
|
version = s.version(backend)
|
||||||
@@ -320,83 +411,68 @@ func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, b
|
|||||||
name = "nftables-docker"
|
name = "nftables-docker"
|
||||||
}
|
}
|
||||||
return dto.DockerPortGuardBase{
|
return dto.DockerPortGuardBase{
|
||||||
|
IPv6Enabled: ipv6Enabled,
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
Backend: backend,
|
Backend: backend,
|
||||||
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || ipv6.Reason != dockerfirewall.ReasonCommandMissing,
|
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
|
||||||
Initialized: ipv4.Initialized || ipv6.Initialized,
|
Initialized: ipv4.Initialized || ipv6.Initialized,
|
||||||
Bound: ipv4.Bound || ipv6.Bound,
|
Bound: ipv4.Bound || ipv6.Bound,
|
||||||
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||||
IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint {
|
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||||
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies))
|
matched := make(map[int]bool, len(policies))
|
||||||
for _, policy := range policies {
|
byEndpoint := make(map[string]int, len(policies))
|
||||||
sources := []string{}
|
for index, policy := range policies {
|
||||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
|
||||||
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
|
if _, exists := byEndpoint[key]; !exists {
|
||||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
byEndpoint[key] = index
|
||||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources,
|
|
||||||
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
|
|
||||||
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return endpoints
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []model.DockerPortGuardPolicy, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
|
||||||
byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies))
|
|
||||||
for _, policy := range policies {
|
|
||||||
byEndpoint[fmt.Sprintf("%s|%s|%d|%s", policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy
|
|
||||||
}
|
}
|
||||||
for i := range endpoints {
|
for i := range endpoints {
|
||||||
key := fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol)
|
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
|
||||||
policy, ok := byEndpoint[key]
|
index, exists := byEndpoint[key]
|
||||||
if !ok {
|
if !exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sources := []string{}
|
policy := policies[index]
|
||||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
|
||||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, sources
|
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
||||||
endpoints[i].Description = policy.Description
|
matched[index] = true
|
||||||
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
|
|
||||||
((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
|
||||||
delete(byEndpoint, key)
|
|
||||||
}
|
}
|
||||||
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
|
orphans := make([]dto.DockerPortGuardEndpoint, 0)
|
||||||
for _, policy := range byEndpoint {
|
for i, policy := range policies {
|
||||||
sources := []string{}
|
if !matched[i] {
|
||||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
orphans = append(orphans, policy)
|
||||||
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
|
|
||||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
|
||||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description,
|
|
||||||
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
|
|
||||||
ManagementReason: dockerReasonNoMatchingPath,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return endpoints, orphanPolicies
|
}
|
||||||
|
return endpoints, orphans
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
|
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
|
||||||
if s.client == nil || len(requested) == 0 {
|
if s.client == nil || len(requested) == 0 {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
cli, err := s.client()
|
cli, err := s.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
info, err := cli.Info(ctx)
|
info, err := cli.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
|
|
||||||
targets := make(map[string]string, len(endpoints))
|
targets := make(map[string]string, len(endpoints))
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -18,7 +18,10 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,19 +31,25 @@ type IFirewallSettingService interface {
|
|||||||
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||||
Load(context.Context) (dto.FirewallSettings, error)
|
Load(context.Context) (dto.FirewallSettings, error)
|
||||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||||
|
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||||
|
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSettingService struct{}
|
type FirewallSettingService struct{}
|
||||||
|
|
||||||
var firewallWhitelistMu sync.Mutex
|
var firewallWhitelistMu sync.Mutex
|
||||||
|
|
||||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
|
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||||
firewallWhitelistMu.Lock()
|
firewallWhitelistMu.Lock()
|
||||||
defer firewallWhitelistMu.Unlock()
|
|
||||||
firewallRuleMutationMu.Lock()
|
firewallRuleMutationMu.Lock()
|
||||||
defer firewallRuleMutationMu.Unlock()
|
defer func() {
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
firewallRuleMutationMu.Unlock()
|
||||||
current, err := loadPortWhitelistSetting(tx)
|
firewallWhitelistMu.Unlock()
|
||||||
|
if result == nil {
|
||||||
|
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
current, err := loadFirewallPortWhiteList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,25 +58,27 @@ func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
value, err := json.Marshal(current)
|
value, err := json.Marshal(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
|
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||||
firewallWhitelistMu.Lock()
|
firewallWhitelistMu.Lock()
|
||||||
defer firewallWhitelistMu.Unlock()
|
|
||||||
firewallRuleMutationMu.Lock()
|
firewallRuleMutationMu.Lock()
|
||||||
defer firewallRuleMutationMu.Unlock()
|
defer func() {
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
firewallRuleMutationMu.Unlock()
|
||||||
current, err := loadPortWhitelistSetting(tx)
|
firewallWhitelistMu.Unlock()
|
||||||
|
if result == nil {
|
||||||
|
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
current, err := loadFirewallPortWhiteList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -80,31 +91,30 @@ func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
value, err := json.Marshal(current)
|
value, err := json.Marshal(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||||
if request.Rule == nil {
|
|
||||||
return fmt.Errorf("select one firewall port whitelist rule to delete")
|
|
||||||
}
|
|
||||||
firewallWhitelistMu.Lock()
|
firewallWhitelistMu.Lock()
|
||||||
defer firewallWhitelistMu.Unlock()
|
|
||||||
firewallRuleMutationMu.Lock()
|
firewallRuleMutationMu.Lock()
|
||||||
defer firewallRuleMutationMu.Unlock()
|
defer func() {
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
firewallRuleMutationMu.Unlock()
|
||||||
current, err := loadPortWhitelistSetting(tx)
|
firewallWhitelistMu.Unlock()
|
||||||
|
}()
|
||||||
|
current, err := loadFirewallPortWhiteList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if request.Rule == nil {
|
||||||
|
return filter.ErrInvalidRule
|
||||||
|
}
|
||||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,20 +124,22 @@ func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
value, err := json.Marshal(current)
|
value, err := json.Marshal(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
|
families, err := loadFirewallFamilies()
|
||||||
|
if err != nil {
|
||||||
|
return dto.FirewallSettings{}, err
|
||||||
|
}
|
||||||
|
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||||
|
|
||||||
installed := make(map[string]bool)
|
installed := make(map[string]bool)
|
||||||
for _, name := range lifecycle.InstalledProviders() {
|
for _, name := range lifecycle.InstalledProviders() {
|
||||||
@@ -153,7 +165,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||||
overview, err := loadSystemFirewallOverview(name, "base")
|
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
}
|
}
|
||||||
@@ -180,18 +192,18 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||||
if option.Installed && name == result.Forwarding.Selected {
|
if option.Installed && name == result.Forwarding.Selected {
|
||||||
manager, err := newForwardingAdapterFor(name)
|
manager, err := newForwardingAdapterFor(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
} else {
|
} else {
|
||||||
status, statusErr := loadForwardingFirewallOverview(manager)
|
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||||
if statusErr != nil {
|
if statusErr != nil {
|
||||||
option.Message = statusErr.Error()
|
option.Message = statusErr.Error()
|
||||||
} else {
|
} else {
|
||||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||||
}
|
}
|
||||||
if name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||||
}
|
}
|
||||||
@@ -228,20 +240,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
option.Active = false
|
option.Active = false
|
||||||
}
|
}
|
||||||
if option.Active {
|
if option.Active {
|
||||||
guard := newDockerFirewallRuntime(name)
|
guard := newDockerFirewallRuntime(ctx, name)
|
||||||
ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
|
for _, family := range families {
|
||||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
status := guard.Status(family)
|
||||||
option.Bound = ipv4.Bound || ipv6.Bound
|
option.Initialized = option.Initialized || status.Initialized
|
||||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
option.Bound = option.Bound || status.Bound
|
||||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
info := dto.FirewallBackendFamilyStatus{
|
||||||
option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
|
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||||
option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
|
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
}
|
||||||
|
if family == constant.FirewallFamilyIPv4 {
|
||||||
|
option.IPv4 = info
|
||||||
|
} else {
|
||||||
|
option.IPv6 = info
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.Docker.Options = append(result.Docker.Options, option)
|
result.Docker.Options = append(result.Docker.Options, option)
|
||||||
}
|
}
|
||||||
var err error
|
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||||
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -256,6 +273,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
|
if request.Operation == "cleanup" {
|
||||||
|
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -273,13 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
|||||||
}
|
}
|
||||||
if request.Operation == "initialize" {
|
if request.Operation == "initialize" {
|
||||||
service := newFirewallService()
|
service := newFirewallService()
|
||||||
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
|
|
||||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||||
return errors.Join(rulesErr, whitelistErr)
|
return whitelistErr
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case "forwarding":
|
case "forwarding":
|
||||||
return s.operateForwarding(request)
|
return s.operateForwarding(ctx, request)
|
||||||
case "docker":
|
case "docker":
|
||||||
return s.operateDocker(ctx, request)
|
return s.operateDocker(ctx, request)
|
||||||
default:
|
default:
|
||||||
@@ -287,6 +307,211 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||||
|
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||||
|
}
|
||||||
|
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||||
|
}
|
||||||
|
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||||
|
}
|
||||||
|
subsystem := ""
|
||||||
|
switch request.Subsystem {
|
||||||
|
case "system":
|
||||||
|
subsystem = firewallTaskHost
|
||||||
|
case "forwarding":
|
||||||
|
subsystem = firewallTaskForwarding
|
||||||
|
case "docker":
|
||||||
|
subsystem = firewallTaskDocker
|
||||||
|
default:
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
|
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||||
|
families, err := loadFirewallFamilies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !slices.Contains(families, request.Family) {
|
||||||
|
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||||
|
}
|
||||||
|
initialize := request.Operation != "bind"
|
||||||
|
switch request.Subsystem {
|
||||||
|
case "system":
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ports, err := loadFirewallPortWhiteList()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
required, err := firewall.RequiredPortWhitelist(ports)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
if request.Backend == constant.FirewallProviderIptables {
|
||||||
|
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||||
|
} else {
|
||||||
|
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||||
|
}
|
||||||
|
firewallRuleMutationMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initialize {
|
||||||
|
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||||
|
case "forwarding":
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if manager.Name() != request.Backend {
|
||||||
|
return filter.ErrProviderUnavailable
|
||||||
|
}
|
||||||
|
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rules, err := manager.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := persistForwardingRules(manager, rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||||
|
default:
|
||||||
|
dockerPortGuardServiceMu.Lock()
|
||||||
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
|
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if backend != request.Backend {
|
||||||
|
return filter.ErrProviderUnavailable
|
||||||
|
}
|
||||||
|
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inventory, err := runtime.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := persistDockerRules(backend, inventory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||||
|
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
defer firewallRuleMutationMu.Unlock()
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
dockerPortGuardServiceMu.Lock()
|
||||||
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
|
if request.Status == constant.StatusEnable {
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||||
|
}
|
||||||
|
ports, err := loadFirewallPortWhiteList()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installed := lifecycle.InstalledProviders()
|
||||||
|
for _, selection := range []struct{ subsystem, key string }{
|
||||||
|
{"system", constant.FirewallSystemBackendKey},
|
||||||
|
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||||
|
{"docker", constant.FirewallDockerBackendKey},
|
||||||
|
} {
|
||||||
|
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||||
|
if backend == "" {
|
||||||
|
if selection.subsystem == "system" {
|
||||||
|
if len(installed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client, err := NewSelectedSystemFirewallClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backend = client.Name()
|
||||||
|
} else {
|
||||||
|
backend = constant.FirewallProviderIptables
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||||
|
switch selection.subsystem {
|
||||||
|
case "system":
|
||||||
|
if backend == constant.FirewallProviderIptables {
|
||||||
|
err = iptables_helper.UnbindIPv6BaseChains()
|
||||||
|
} else {
|
||||||
|
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||||
|
if err == nil {
|
||||||
|
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "forwarding":
|
||||||
|
var manager forwarding.Adapter
|
||||||
|
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||||
|
if err == nil {
|
||||||
|
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||||
|
}
|
||||||
|
case "docker":
|
||||||
|
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := t.TaskCtx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func NewIFirewallSettingService() IFirewallSettingService {
|
func NewIFirewallSettingService() IFirewallSettingService {
|
||||||
return &FirewallSettingService{}
|
return &FirewallSettingService{}
|
||||||
}
|
}
|
||||||
@@ -297,9 +522,6 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
|||||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "cleanup" {
|
|
||||||
return cleanupSystemBackend(request.Backend)
|
|
||||||
}
|
|
||||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||||
if previous == "" {
|
if previous == "" {
|
||||||
if client, err := lifecycle.NewClient(""); err == nil {
|
if client, err := lifecycle.NewClient(""); err == nil {
|
||||||
@@ -363,32 +585,21 @@ func systemFirewallBackendInitialized(backend string) (bool, error) {
|
|||||||
return client.Status()
|
return client.Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
manager, err := newForwardingAdapterFor(request.Backend)
|
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "cleanup" {
|
|
||||||
if err := manager.Cleanup(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
recordForwardingSyncError(nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
if request.Operation == "select" {
|
if request.Operation == "select" {
|
||||||
current := previous
|
current := previous
|
||||||
if current == "" {
|
if current == "" {
|
||||||
detected, err := newForwardingAdapter()
|
detected, err := newForwardingAdapter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
current = detected.Name()
|
current = detected.Name()
|
||||||
}
|
}
|
||||||
initialized, err := forwardingBackendInitialized(current)
|
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -400,14 +611,13 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "initialize" {
|
if request.Operation == "initialize" {
|
||||||
return newForwardingService().Enable()
|
return newForwardingService().Enable(ctx)
|
||||||
}
|
}
|
||||||
recordForwardingSyncError(nil)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||||
manager, err := newForwardingAdapterFor(backend)
|
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -427,13 +637,7 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
guard := newDockerFirewallRuntime(request.Backend)
|
|
||||||
if request.Operation == "cleanup" {
|
|
||||||
if err := guard.Cleanup(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
|
||||||
}
|
|
||||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
if request.Operation == "select" {
|
if request.Operation == "select" {
|
||||||
current := previous
|
current := previous
|
||||||
@@ -443,7 +647,7 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
|
|||||||
current = constant.FirewallProviderIptables
|
current = constant.FirewallProviderIptables
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initialized, err := dockerGuardBackendInitialized(current)
|
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -469,8 +673,8 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||||
guard := newDockerFirewallRuntime(backend)
|
guard := newDockerFirewallRuntime(ctx, backend)
|
||||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||||
initialized, err := guard.Initialized(family)
|
initialized, err := guard.Initialized(family)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
||||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
|
||||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
firewallRuleSyncTaskMu sync.Mutex
|
|
||||||
firewallRuleSyncTaskID string
|
|
||||||
)
|
|
||||||
|
|
||||||
type firewallDatabaseSyncAdapter interface {
|
|
||||||
previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
|
|
||||||
syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
|
|
||||||
firewallWhitelistMu.Lock()
|
|
||||||
defer firewallWhitelistMu.Unlock()
|
|
||||||
|
|
||||||
ports, err := loadFirewallPortWhiteList()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
provider, err := s.selectedProvider(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := s.syncPortWhitelist(ctx, provider, ports); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.removeTransferredSystemPortRules(ctx, provider, ports)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
||||||
switch strings.TrimSpace(request.Subsystem) {
|
|
||||||
case "forwarding":
|
|
||||||
service := s.forwardingSync
|
|
||||||
if service == nil {
|
|
||||||
service = newForwardingService()
|
|
||||||
}
|
|
||||||
return service.previewRuleSync(ctx, request)
|
|
||||||
case "docker":
|
|
||||||
service := s.dockerSync
|
|
||||||
if service == nil {
|
|
||||||
service = newDockerPortGuardService()
|
|
||||||
}
|
|
||||||
return service.previewRuleSync(ctx, request)
|
|
||||||
}
|
|
||||||
firewallRuleMutationMu.Lock()
|
|
||||||
defer firewallRuleMutationMu.Unlock()
|
|
||||||
_, rules, _, err := s.loadFirewallSyncRules(ctx, request)
|
|
||||||
preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))}
|
|
||||||
for _, rule := range rules {
|
|
||||||
preview.Add(rule.FirewallRuleSyncItem)
|
|
||||||
}
|
|
||||||
return preview, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallService) SyncRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
||||||
switch strings.TrimSpace(request.Subsystem) {
|
|
||||||
case "forwarding":
|
|
||||||
service := s.forwardingSync
|
|
||||||
if service == nil {
|
|
||||||
service = newForwardingService()
|
|
||||||
}
|
|
||||||
return service.syncRules(ctx, request)
|
|
||||||
case "docker":
|
|
||||||
service := s.dockerSync
|
|
||||||
if service == nil {
|
|
||||||
service = newDockerPortGuardService()
|
|
||||||
}
|
|
||||||
return service.syncRules(ctx, request)
|
|
||||||
default:
|
|
||||||
return s.syncSystemRules(ctx, clientIP, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) {
|
|
||||||
firewallRuleSyncTaskMu.Lock()
|
|
||||||
defer firewallRuleSyncTaskMu.Unlock()
|
|
||||||
return currentFirewallRuleSyncTaskLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
|
|
||||||
dockerPortGuardServiceMu.Lock()
|
|
||||||
defer dockerPortGuardServiceMu.Unlock()
|
|
||||||
return s.reconcileLocked(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ForwardingService) Restore(ctx context.Context) error {
|
|
||||||
forwardingMutationMu.Lock()
|
|
||||||
defer forwardingMutationMu.Unlock()
|
|
||||||
enabled, err := s.forwardingEnabled()
|
|
||||||
if err != nil || !enabled {
|
|
||||||
if err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
manager, err := s.clientFactory()
|
|
||||||
if err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stored, err := s.rules.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.initializeForwarding(manager); err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = manager.ReplaceRules(forwardingRulesFromModels(stored))
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ForwardingService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
||||||
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncPreview{}, err
|
|
||||||
}
|
|
||||||
target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncPreview{}, err
|
|
||||||
}
|
|
||||||
return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ForwardingService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
||||||
forwardingMutationMu.Lock()
|
|
||||||
defer forwardingMutationMu.Unlock()
|
|
||||||
|
|
||||||
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules)
|
|
||||||
desired := make([]forwarding.Rule, 0, len(candidates))
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
if candidate.err == nil {
|
|
||||||
desired = append(desired, candidate.rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if preview.Blocked > 0 {
|
|
||||||
return firewallSyncResult(preview, nil, false), nil
|
|
||||||
}
|
|
||||||
if len(desired) == 0 && !targetInitialized {
|
|
||||||
return firewallSyncResult(preview, nil, true), nil
|
|
||||||
}
|
|
||||||
reconcileErr := func() error {
|
|
||||||
if len(desired) > 0 {
|
|
||||||
if err := s.persistForwardingEnabled(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.initializeForwarding(target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := target.ReplaceRules(desired); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return verifyForwardingRuleSync(target, desired)
|
|
||||||
}()
|
|
||||||
result := firewallSyncResult(preview, reconcileErr, true)
|
|
||||||
recordForwardingSyncError(reconcileErr)
|
|
||||||
if reconcileErr != nil {
|
|
||||||
if preview.Ready == 0 {
|
|
||||||
return result, reconcileErr
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DockerPortGuardService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
||||||
target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncPreview{}, err
|
|
||||||
}
|
|
||||||
targetInventory, err := runtime.ListPolicies()
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncPreview{}, err
|
|
||||||
}
|
|
||||||
return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DockerPortGuardService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
||||||
dockerPortGuardServiceMu.Lock()
|
|
||||||
defer dockerPortGuardServiceMu.Unlock()
|
|
||||||
|
|
||||||
target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
runtimePolicies := make([]dockerfirewall.Policy, 0, len(policies))
|
|
||||||
for _, policy := range policies {
|
|
||||||
sources := []string{}
|
|
||||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
|
||||||
runtimePolicies = append(runtimePolicies, dockerfirewall.Policy{
|
|
||||||
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
|
||||||
Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
targetInventory, err := targetRuntime.ListPolicies()
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory)
|
|
||||||
for _, item := range preview.Items {
|
|
||||||
if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
|
||||||
return firewallSyncResult(preview, nil, false), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if preview.Ready == 0 && preview.Removed == 0 {
|
|
||||||
return firewallSyncResult(preview, nil, false), nil
|
|
||||||
}
|
|
||||||
reconcileErr := func() error {
|
|
||||||
if err := reconcileDockerFirewall(target, runtimePolicies, targetRuntime, targetInventory); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := verifyDockerFirewall(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(policies) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
|
||||||
}()
|
|
||||||
result := firewallSyncResult(preview, reconcileErr, true)
|
|
||||||
if reconcileErr != nil {
|
|
||||||
return result, reconcileErr
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallService) syncSystemRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
||||||
subsystem := strings.TrimSpace(request.Subsystem)
|
|
||||||
if subsystem == "" {
|
|
||||||
subsystem = "system"
|
|
||||||
}
|
|
||||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
defer firewallLifecycleTaskMu.Unlock()
|
|
||||||
firewallRuleSyncTaskMu.Lock()
|
|
||||||
defer firewallRuleSyncTaskMu.Unlock()
|
|
||||||
|
|
||||||
running, err := currentFirewallRuleSyncTaskLocked()
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, err
|
|
||||||
}
|
|
||||||
if running.Executing {
|
|
||||||
return dto.FirewallRuleSyncResult{
|
|
||||||
Subsystem: subsystem,
|
|
||||||
TargetProvider: request.TargetProvider,
|
|
||||||
TaskID: running.TaskID,
|
|
||||||
Queued: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if subsystem != "system" {
|
|
||||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
|
|
||||||
}
|
|
||||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err)
|
|
||||||
}
|
|
||||||
taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error {
|
|
||||||
result, err := s.syncRules(t.TaskCtx, clientIP, request, t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if result.Failed > 0 {
|
|
||||||
return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed}))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, nil, 0, 0)
|
|
||||||
|
|
||||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
|
||||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
|
||||||
closeUnstartedFirewallTask(taskItem)
|
|
||||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err)
|
|
||||||
}
|
|
||||||
firewallRuleSyncTaskID = taskItem.TaskID
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
firewallRuleSyncTaskMu.Lock()
|
|
||||||
if firewallRuleSyncTaskID == taskItem.TaskID {
|
|
||||||
firewallRuleSyncTaskID = ""
|
|
||||||
}
|
|
||||||
firewallRuleSyncTaskMu.Unlock()
|
|
||||||
}()
|
|
||||||
if err := taskItem.Execute(); err != nil && global.LOG != nil {
|
|
||||||
global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return dto.FirewallRuleSyncResult{
|
|
||||||
Subsystem: "system",
|
|
||||||
TargetProvider: request.TargetProvider,
|
|
||||||
TaskID: taskItem.TaskID,
|
|
||||||
Queued: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyForwardingRuleSync(target forwarding.Adapter, desired []forwarding.Rule) error {
|
|
||||||
actual, err := target.List()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
|
||||||
}
|
|
||||||
actual, err = normalizeForwardingRuntimeRules(actual)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
|
||||||
}
|
|
||||||
if !firewallRuleStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
|
|
||||||
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reconcileDockerFirewall(backend string, policies []dockerfirewall.Policy, runtime dockerfirewall.Runtime, inventory dockerfirewall.PolicyInventory) error {
|
|
||||||
families := make(map[string]struct{}, len(policies))
|
|
||||||
needsInitialize, needsBind := false, false
|
|
||||||
for _, policy := range policies {
|
|
||||||
families[policy.Family] = struct{}{}
|
|
||||||
}
|
|
||||||
if len(families) == 0 {
|
|
||||||
initialized := false
|
|
||||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
|
||||||
status := runtime.Status(family)
|
|
||||||
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
|
||||||
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
|
|
||||||
}
|
|
||||||
initialized = initialized || status.Initialized
|
|
||||||
}
|
|
||||||
if initialized {
|
|
||||||
return runtime.ReplacePolicies(nil, inventory)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for family := range families {
|
|
||||||
status := runtime.Status(family)
|
|
||||||
needsInitialize = needsInitialize || !status.Initialized
|
|
||||||
needsBind = needsBind || !status.Bound || !status.Effective
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
if needsInitialize {
|
|
||||||
err = runtime.Initialize(policies, inventory)
|
|
||||||
} else {
|
|
||||||
if needsBind {
|
|
||||||
err = runtime.Bind()
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = runtime.ReplacePolicies(policies, inventory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for family := range families {
|
|
||||||
if !runtime.Status(family).Effective {
|
|
||||||
return fmt.Errorf("Docker firewall target %s is not effective for %s", backend, family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
|
|
||||||
if err := ReconcileDockerPortGuard(ctx); err != nil {
|
|
||||||
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1698
-2837
File diff suppressed because it is too large
Load Diff
+138
-416
@@ -5,71 +5,55 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
forwardingSyncConverged = "converged"
|
|
||||||
forwardingSyncMissing = "missing"
|
|
||||||
forwardingSyncRuntimeOnly = "runtime_only"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IForwardingService interface {
|
type IForwardingService interface {
|
||||||
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error)
|
||||||
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||||
|
SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||||
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
|
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
|
||||||
Enable() error
|
Enable(ctx context.Context) error
|
||||||
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
|
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
|
||||||
Restore(context.Context) error
|
Restore(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardingService struct {
|
type ForwardingService struct {
|
||||||
clientFactory func() (forwarding.Adapter, error)
|
clientFactory func(context.Context) (forwarding.Adapter, error)
|
||||||
rules repo.IForwardingRuleRepo
|
|
||||||
enabled func() (bool, error)
|
|
||||||
persistBackend func(string) error
|
|
||||||
markEnabled func() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
||||||
|
|
||||||
var forwardingMutationMu sync.Mutex
|
var forwardingMutationMu sync.Mutex
|
||||||
|
|
||||||
var (
|
func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) {
|
||||||
forwardingSyncStateMu sync.RWMutex
|
families, err := loadFirewallFamilies()
|
||||||
forwardingLastSyncErr error
|
if err != nil {
|
||||||
)
|
return dto.FirewallSubsystemStatus{}, err
|
||||||
|
}
|
||||||
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
|
|
||||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
selected = strings.TrimSpace(selected)
|
selected = strings.TrimSpace(selected)
|
||||||
if selected == "" {
|
if selected == "" {
|
||||||
selected = constant.FirewallProviderIptables
|
selected = constant.FirewallProviderIptables
|
||||||
}
|
}
|
||||||
baseInfo := dto.FirewallSubsystemStatus{
|
baseInfo := dto.FirewallSubsystemStatus{
|
||||||
Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(),
|
Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1,
|
||||||
}
|
}
|
||||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||||
baseInfo.Name += "-forward"
|
baseInfo.Name += "-forward"
|
||||||
}
|
}
|
||||||
manager, err := s.clientFactory()
|
manager, err := s.clientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errForwardingBackendUnavailable) {
|
if errors.Is(err, errForwardingBackendUnavailable) {
|
||||||
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
||||||
@@ -82,7 +66,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
|||||||
return baseInfo, err
|
return baseInfo, err
|
||||||
}
|
}
|
||||||
version, versionErr := client.Version()
|
version, versionErr := client.Version()
|
||||||
status, statusErr := loadForwardingFirewallOverview(manager)
|
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||||
if err := errors.Join(versionErr, statusErr); err != nil {
|
if err := errors.Join(versionErr, statusErr); err != nil {
|
||||||
return baseInfo, err
|
return baseInfo, err
|
||||||
}
|
}
|
||||||
@@ -95,83 +79,51 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
|||||||
baseInfo.PingStatus = firewall.LoadPingStatus()
|
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||||
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||||
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
||||||
for _, family := range []struct {
|
|
||||||
command string
|
|
||||||
status *dto.FirewallBackendFamilyStatus
|
|
||||||
}{
|
|
||||||
{"iptables", &baseInfo.IPv4},
|
|
||||||
{"ip6tables", &baseInfo.IPv6},
|
|
||||||
} {
|
|
||||||
policy, err := loadForwardPolicy(family.command)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
family.status.ForwardPolicy = policy
|
|
||||||
}
|
|
||||||
return baseInfo, nil
|
return baseInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||||
if request.Strategy != "" {
|
var manager forwarding.Adapter
|
||||||
return 0, nil, nil
|
var err error
|
||||||
|
if provider == "" {
|
||||||
|
manager, err = s.clientFactory(ctx)
|
||||||
|
} else {
|
||||||
|
manager, err = newForwardingAdapterFor(ctx, string(provider))
|
||||||
}
|
}
|
||||||
stored, err := s.rules.List(context.Background())
|
if err != nil {
|
||||||
|
return dto.FirewallSubsystemBackup{}, err
|
||||||
|
}
|
||||||
|
rules, err := manager.List()
|
||||||
|
if err != nil {
|
||||||
|
return dto.FirewallSubsystemBackup{}, err
|
||||||
|
}
|
||||||
|
return forwardingBackup(manager, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
||||||
|
manager, err := s.clientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
manager, err := s.clientFactory()
|
rules, err := manager.List()
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
runtime, err := manager.List()
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
inventory, err := mergeForwardingInventory(stored, runtime)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
items := make([]dto.ForwardRule, 0, len(rules))
|
||||||
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
||||||
filtered := inventory[:0]
|
for _, rule := range rules {
|
||||||
for _, item := range inventory {
|
if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) {
|
||||||
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
|
continue
|
||||||
filtered = append(filtered, item)
|
|
||||||
}
|
}
|
||||||
|
items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||||
}
|
}
|
||||||
inventory = filtered
|
total := len(items)
|
||||||
total := len(inventory)
|
|
||||||
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
|
|
||||||
if request.All {
|
if request.All {
|
||||||
start, end = 0, total
|
|
||||||
}
|
|
||||||
if start > total {
|
|
||||||
return int64(total), make([]dto.ForwardRule, 0), nil
|
|
||||||
}
|
|
||||||
if end > total {
|
|
||||||
end = total
|
|
||||||
}
|
|
||||||
pageRules := inventory[start:end]
|
|
||||||
var items []dto.ForwardRule
|
|
||||||
if pageRules != nil {
|
|
||||||
items = make([]dto.ForwardRule, 0, len(pageRules))
|
|
||||||
}
|
|
||||||
for index, item := range pageRules {
|
|
||||||
items = append(items, dto.ForwardRule{
|
|
||||||
ID: item.ID,
|
|
||||||
Num: strconv.Itoa(start + index + 1),
|
|
||||||
Family: item.Rule.Family,
|
|
||||||
Protocol: item.Rule.Protocol,
|
|
||||||
Port: item.Rule.Port,
|
|
||||||
TargetIP: item.Rule.TargetIP,
|
|
||||||
TargetPort: item.Rule.TargetPort,
|
|
||||||
Interface: item.Rule.Interface,
|
|
||||||
IsDesired: item.IsDesired,
|
|
||||||
IsRuntime: item.IsRuntime,
|
|
||||||
SyncStatus: item.SyncStatus(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return int64(total), items, nil
|
return int64(total), items, nil
|
||||||
|
}
|
||||||
|
start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total)
|
||||||
|
end := min(start+max(request.PageSize, 1), total)
|
||||||
|
return int64(total), items[start:end], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
||||||
@@ -198,7 +150,7 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
|
|||||||
return dto.FilterChainOperationResponse{}, err
|
return dto.FilterChainOperationResponse{}, err
|
||||||
}
|
}
|
||||||
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
||||||
return s.operateRules(t.TaskCtx, request, t)
|
return s.operateRules(t.TaskCtx, request, t, false)
|
||||||
}, nil, 0, 0)
|
}, nil, 0, 0)
|
||||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||||
@@ -209,358 +161,128 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
|
|||||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ForwardingService) Enable() error {
|
func (s *ForwardingService) Enable(ctx context.Context) error {
|
||||||
forwardingMutationMu.Lock()
|
forwardingMutationMu.Lock()
|
||||||
defer forwardingMutationMu.Unlock()
|
defer forwardingMutationMu.Unlock()
|
||||||
manager, err := s.clientFactory()
|
manager, err := s.clientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.persistForwardingEnabled(); err != nil {
|
rules, err := manager.List()
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.initializeForwarding(manager); err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rules, err := s.rules.List(context.Background())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
families, err := loadFirewallFamilies()
|
||||||
recordForwardingSyncError(err)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if err := initializeForwarding(manager, families); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return persistForwardingRules(manager, rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
|
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
return dto.FilterChainOperationResponse{}, err
|
return dto.FilterChainOperationResponse{}, err
|
||||||
}
|
}
|
||||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
var backup dto.FirewallSubsystemBackup
|
||||||
|
if request.BackupFile != "" {
|
||||||
|
var err error
|
||||||
|
backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
|
return dto.FilterChainOperationResponse{}, err
|
||||||
}
|
}
|
||||||
var manager forwarding.Adapter
|
}
|
||||||
var backend string
|
operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))}
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
|
for _, rule := range backup.Forwarding {
|
||||||
|
operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error {
|
||||||
|
return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||||
forwardingMutationMu.Lock()
|
forwardingMutationMu.Lock()
|
||||||
defer forwardingMutationMu.Unlock()
|
defer forwardingMutationMu.Unlock()
|
||||||
var err error
|
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
|
||||||
manager, err = s.clientFactory()
|
if err != nil || status != constant.StatusEnable {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manager, err := s.clientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
backend = manager.Name()
|
backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding")
|
||||||
t.Logf("backend=%s", backend)
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
if err := s.persistForwardingEnabled(); err != nil {
|
return nil
|
||||||
recordForwardingSyncError(err)
|
}
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.initializeForwarding(manager); err != nil {
|
families, err := loadFirewallFamilies()
|
||||||
recordForwardingSyncError(err)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||||
|
before := len(backup.Forwarding)
|
||||||
|
backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 })
|
||||||
|
logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding))
|
||||||
|
}
|
||||||
|
missing := make(map[string]bool)
|
||||||
|
needsBind := false
|
||||||
|
for _, family := range families {
|
||||||
|
initialized, bound, err := manager.FamilyStatus(family)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
missing[family] = !initialized
|
||||||
|
needsBind = needsBind || (initialized && !bound)
|
||||||
|
}
|
||||||
|
restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6)
|
||||||
|
for _, rule := range backup.Forwarding {
|
||||||
|
restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6
|
||||||
|
}
|
||||||
|
if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
current, err := manager.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
seen := make(map[string]int, len(current))
|
||||||
|
for _, rule := range current {
|
||||||
|
seen[rule.Identity()]++
|
||||||
|
}
|
||||||
|
for _, family := range families {
|
||||||
|
if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := manager.OperateFamily(family, missing[family]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !missing[family] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rules := make([]forwarding.Rule, 0)
|
||||||
|
for _, rule := range backup.Forwarding {
|
||||||
|
if rule.Family != family {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[rule.Identity()] > 0 {
|
||||||
|
seen[rule.Identity()]--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
if err := manager.CreateRules(ctx, rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
|
|
||||||
forwardingMutationMu.Lock()
|
|
||||||
defer forwardingMutationMu.Unlock()
|
|
||||||
rules, err := s.rules.List(t.TaskCtx)
|
|
||||||
if err != nil {
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
|
||||||
recordForwardingSyncError(err)
|
|
||||||
return err
|
|
||||||
}, nil)
|
|
||||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
|
|
||||||
}
|
|
||||||
go func() { _ = taskItem.Execute() }()
|
|
||||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIForwardingService() IForwardingService {
|
func NewIForwardingService() IForwardingService {
|
||||||
return newForwardingService()
|
return newForwardingService()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadForwardPolicy(command string) (string, error) {
|
|
||||||
if !cmd.Which(command) {
|
|
||||||
command += "-nft"
|
|
||||||
if !cmd.Which(command) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(output, "\n") {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
|
|
||||||
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
|
|
||||||
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
|
|
||||||
}
|
|
||||||
return fields[2], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("FORWARD default policy was not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func lastForwardingSyncError() string {
|
|
||||||
forwardingSyncStateMu.RLock()
|
|
||||||
defer forwardingSyncStateMu.RUnlock()
|
|
||||||
if forwardingLastSyncErr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return forwardingLastSyncErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
|
|
||||||
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
|
|
||||||
byIdentity := make(map[string]int, len(stored)+len(runtime))
|
|
||||||
for _, record := range stored {
|
|
||||||
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
|
||||||
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
|
|
||||||
TargetPort: record.TargetPort, Interface: record.Interface,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("normalize desired forwarding rule: %w", err)
|
|
||||||
}
|
|
||||||
key := rule.Identity()
|
|
||||||
byIdentity[key] = len(items)
|
|
||||||
items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true})
|
|
||||||
}
|
|
||||||
for _, observed := range runtime {
|
|
||||||
rule, err := forwarding.NormalizeRule(observed)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err)
|
|
||||||
}
|
|
||||||
key := rule.Identity()
|
|
||||||
if index, exists := byIdentity[key]; exists {
|
|
||||||
items[index].IsRuntime = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byIdentity[key] = len(items)
|
|
||||||
items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true})
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
|
|
||||||
values := []string{
|
|
||||||
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
|
|
||||||
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
|
|
||||||
}
|
|
||||||
for _, value := range values {
|
|
||||||
if strings.Contains(strings.ToLower(value), keyword) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task) (resultErr error) {
|
|
||||||
forwardingMutationMu.Lock()
|
|
||||||
defer forwardingMutationMu.Unlock()
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
type operationBatch struct {
|
|
||||||
operation forwarding.OperationType
|
|
||||||
rules []forwarding.Rule
|
|
||||||
}
|
|
||||||
groups := make([]operationBatch, 0)
|
|
||||||
for _, operation := range request.Rules {
|
|
||||||
kind := forwarding.OperationType(operation.Operation)
|
|
||||||
if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove {
|
|
||||||
return fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
|
|
||||||
}
|
|
||||||
if len(groups) == 0 || groups[len(groups)-1].operation != kind {
|
|
||||||
groups = append(groups, operationBatch{operation: kind})
|
|
||||||
}
|
|
||||||
for _, protocol := range strings.Split(operation.Protocol, "/") {
|
|
||||||
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
|
||||||
Family: operation.Family, Protocol: protocol, Port: operation.Port,
|
|
||||||
TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stored, err := s.rules.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
byIdentity := make(map[string]model.ForwardingRule, len(stored))
|
|
||||||
for index, rule := range forwardingRulesFromModels(stored) {
|
|
||||||
normalized, err := forwarding.NormalizeRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
byIdentity[normalized.Identity()] = stored[index]
|
|
||||||
}
|
|
||||||
succeeded, failed, skipped := 0, 0, 0
|
|
||||||
var nativeFailure error
|
|
||||||
defer func() {
|
|
||||||
recordForwardingSyncError(errors.Join(resultErr, nativeFailure))
|
|
||||||
if t != nil {
|
|
||||||
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed}))
|
|
||||||
if skipped > 0 {
|
|
||||||
t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) {
|
|
||||||
label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
|
|
||||||
switch status {
|
|
||||||
case "skipped":
|
|
||||||
skipped++
|
|
||||||
if t != nil {
|
|
||||||
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
|
|
||||||
}
|
|
||||||
case "failed":
|
|
||||||
failed++
|
|
||||||
if t != nil {
|
|
||||||
t.LogFailedWithErr(label, cause)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
succeeded++
|
|
||||||
if t != nil {
|
|
||||||
t.LogSuccess(label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd {
|
|
||||||
old := make(map[string]bool, len(groups[0].rules))
|
|
||||||
for _, rule := range groups[0].rules {
|
|
||||||
old[rule.Identity()] = true
|
|
||||||
}
|
|
||||||
unchanged := len(old) == len(groups[1].rules)
|
|
||||||
duplicate := false
|
|
||||||
for _, rule := range groups[1].rules {
|
|
||||||
key := rule.Identity()
|
|
||||||
unchanged = unchanged && old[key]
|
|
||||||
if _, exists := byIdentity[key]; exists && !old[key] {
|
|
||||||
duplicate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if unchanged || duplicate {
|
|
||||||
for _, group := range groups {
|
|
||||||
for _, rule := range group.rules {
|
|
||||||
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, rule := range groups[1].rules {
|
|
||||||
if rule.Family != forwarding.FamilyIPv6 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("check IPv6 Router Advertisement: %w", err)
|
|
||||||
}
|
|
||||||
if len(interfaces) > 0 {
|
|
||||||
return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var client forwarding.Adapter
|
|
||||||
var failures []error
|
|
||||||
for _, group := range groups {
|
|
||||||
byFamily := make(map[string][]forwarding.Rule, 2)
|
|
||||||
seen := make(map[string]bool, len(group.rules))
|
|
||||||
for _, rule := range group.rules {
|
|
||||||
key := rule.Identity()
|
|
||||||
_, exists := byIdentity[key]
|
|
||||||
if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
|
|
||||||
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = true
|
|
||||||
byFamily[rule.Family] = append(byFamily[rule.Family], rule)
|
|
||||||
}
|
|
||||||
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
|
||||||
rules := byFamily[family]
|
|
||||||
if len(rules) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := ctx.Err()
|
|
||||||
if err == nil && client == nil {
|
|
||||||
var enabled bool
|
|
||||||
enabled, err = s.forwardingEnabled()
|
|
||||||
if err == nil && !enabled {
|
|
||||||
err = fmt.Errorf("%w: forwarding is not initialized", filter.ErrProviderUnavailable)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
client, err = s.clientFactory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if group.operation == forwarding.OperationAdd {
|
|
||||||
err = client.CreateRules(ctx, rules)
|
|
||||||
} else {
|
|
||||||
err = client.DeleteRules(ctx, rules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
nativeFailure = errors.Join(nativeFailure, err)
|
|
||||||
if !request.ForceDelete || !forwardingOperationsOnlyRemove(request.Rules) || ctx.Err() != nil {
|
|
||||||
failures = append(failures, err)
|
|
||||||
for _, rule := range rules {
|
|
||||||
record(group.operation, rule, "failed", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if t != nil {
|
|
||||||
t.Logf("force delete database records: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for start := 0; start < len(rules); start += 500 {
|
|
||||||
batch := rules[start:min(start+500, len(rules))]
|
|
||||||
records := make([]model.ForwardingRule, 0, len(batch))
|
|
||||||
ids := make([]uint, 0, len(batch))
|
|
||||||
for _, rule := range batch {
|
|
||||||
if group.operation == forwarding.OperationAdd {
|
|
||||||
records = append(records, model.ForwardingRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
|
||||||
} else if stored, exists := byIdentity[rule.Identity()]; exists {
|
|
||||||
ids = append(ids, stored.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if group.operation == forwarding.OperationAdd {
|
|
||||||
err = s.rules.CreateBatch(context.WithoutCancel(ctx), records)
|
|
||||||
} else {
|
|
||||||
err = s.rules.DeleteBatch(context.WithoutCancel(ctx), ids)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
failures = append(failures, err)
|
|
||||||
}
|
|
||||||
for index, rule := range batch {
|
|
||||||
if err != nil {
|
|
||||||
record(group.operation, rule, "failed", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if group.operation == forwarding.OperationAdd {
|
|
||||||
byIdentity[rule.Identity()] = records[index]
|
|
||||||
} else {
|
|
||||||
delete(byIdentity, rule.Identity())
|
|
||||||
}
|
|
||||||
record(group.operation, rule, "succeeded", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if group.operation == forwarding.OperationRemove && len(failures) > 0 {
|
|
||||||
return errors.Join(failures...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.Join(failures...)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Release names at https://github.com/NousResearch/hermes-agent/releases pair
|
||||||
|
// these historical calendar tags with their semantic versions. This includes
|
||||||
|
// every calendar version published in the 1Panel app store. Do not rewrite
|
||||||
|
// stored versions: backup metadata, restore and rollback retain the original tag.
|
||||||
|
var hermesHistoricalVersions = map[string]string{
|
||||||
|
"2026.3.12": "0.2.0",
|
||||||
|
"2026.3.17": "0.3.0",
|
||||||
|
"2026.3.23": "0.4.0",
|
||||||
|
"2026.3.28": "0.5.0",
|
||||||
|
"2026.3.30": "0.6.0",
|
||||||
|
"2026.4.3": "0.7.0",
|
||||||
|
"2026.4.8": "0.8.0",
|
||||||
|
"2026.4.13": "0.9.0",
|
||||||
|
"2026.4.16": "0.10.0",
|
||||||
|
"2026.4.23": "0.11.0",
|
||||||
|
"2026.4.30": "0.12.0",
|
||||||
|
"2026.5.7": "0.13.0",
|
||||||
|
"2026.5.16": "0.14.0",
|
||||||
|
"2026.5.28": "0.15.0",
|
||||||
|
"2026.5.29": "0.15.1",
|
||||||
|
"2026.5.29.2": "0.15.2",
|
||||||
|
"2026.6.5": "0.16.0",
|
||||||
|
"2026.6.19": "0.17.0",
|
||||||
|
"2026.7.1": "0.18.0",
|
||||||
|
"2026.7.7": "0.18.1",
|
||||||
|
"2026.7.7.2": "0.18.2",
|
||||||
|
"2026.7.20": "0.19.0",
|
||||||
|
"2026.7.30": "0.19.1",
|
||||||
|
"2026.8.3": "0.20.0",
|
||||||
|
"2026.8.13": "0.20.1",
|
||||||
|
"2026.8.16": "0.20.2",
|
||||||
|
"2026.8.16.2": "0.20.3",
|
||||||
|
"2026.8.18": "0.20.4",
|
||||||
|
"2026.8.19": "0.20.5",
|
||||||
|
"2026.8.27": "0.20.6",
|
||||||
|
"2026.8.31": "0.21.0",
|
||||||
|
"2026.9.7": "0.21.1",
|
||||||
|
"2026.9.11": "0.21.2",
|
||||||
|
"2026.9.14": "0.21.3",
|
||||||
|
"2026.9.21": "0.21.4",
|
||||||
|
"2026.9.24": "0.21.5",
|
||||||
|
}
|
||||||
|
|
||||||
|
var hermesReleaseVersion = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
|
||||||
|
|
||||||
|
func isHermesApp(key string) bool {
|
||||||
|
return key == constant.AppHermesAgent || key == "local"+constant.AppHermesAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
func hermesComparableVersion(version string) (string, bool) {
|
||||||
|
version = strings.TrimPrefix(version, "v")
|
||||||
|
if mapped, ok := hermesHistoricalVersions[version]; ok {
|
||||||
|
return mapped, true
|
||||||
|
}
|
||||||
|
parts := hermesReleaseVersion.FindStringSubmatch(version)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for i, part := range parts[1:] {
|
||||||
|
n, err := strconv.Atoi(part)
|
||||||
|
if err != nil || (i == 0 && n >= 2000) {
|
||||||
|
// An unknown calendar tag must not become a huge semantic major.
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return version, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNewerAppVersion(key, target, current string) bool {
|
||||||
|
if !isHermesApp(key) {
|
||||||
|
return common.CompareVersion(target, current)
|
||||||
|
}
|
||||||
|
targetVersion, targetOK := hermesComparableVersion(target)
|
||||||
|
currentVersion, currentOK := hermesComparableVersion(current)
|
||||||
|
return targetOK && currentOK && common.CompareVersion(targetVersion, currentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCrossAppVersion(key, current, target string) bool {
|
||||||
|
if !isHermesApp(key) {
|
||||||
|
return common.IsCrossVersion(current, target)
|
||||||
|
}
|
||||||
|
currentVersion, currentOK := hermesComparableVersion(current)
|
||||||
|
targetVersion, targetOK := hermesComparableVersion(target)
|
||||||
|
return currentOK && targetOK && common.IsCrossVersion(currentVersion, targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appVersionSortBefore(key, left, right string) bool {
|
||||||
|
if !isHermesApp(key) {
|
||||||
|
return common.CompareVersion(left, right)
|
||||||
|
}
|
||||||
|
lv, lok := hermesComparableVersion(left)
|
||||||
|
rv, rok := hermesComparableVersion(right)
|
||||||
|
if lok != rok {
|
||||||
|
return lok
|
||||||
|
}
|
||||||
|
if !lok {
|
||||||
|
return common.CompareVersion(left, right)
|
||||||
|
}
|
||||||
|
if lv != rv {
|
||||||
|
return common.CompareVersion(lv, rv)
|
||||||
|
}
|
||||||
|
// Prefer the official semantic spelling when both aliases are available.
|
||||||
|
return strings.TrimPrefix(left, "v") == lv && strings.TrimPrefix(right, "v") != rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAppVersions(key string, versions []string) []string {
|
||||||
|
if !isHermesApp(key) {
|
||||||
|
return common.GetSortedVersions(versions)
|
||||||
|
}
|
||||||
|
sort.SliceStable(versions, func(i, j int) bool {
|
||||||
|
return appVersionSortBefore(key, versions[i], versions[j])
|
||||||
|
})
|
||||||
|
return versions
|
||||||
|
}
|
||||||
+31
-16
@@ -28,7 +28,8 @@ import (
|
|||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/moby/go-archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageService struct{}
|
type ImageService struct{}
|
||||||
@@ -325,18 +326,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
taskItem.Log("Failed to create Docker client: " + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
for _, itemPath := range req.Paths {
|
for _, itemPath := range req.Paths {
|
||||||
currentPath := itemPath
|
currentPath := itemPath
|
||||||
itemName := path.Base(currentPath)
|
itemName := path.Base(currentPath)
|
||||||
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
|
||||||
taskItem.Logf("----------------- %s -----------------", itemName)
|
taskItem.Logf("----------------- %s -----------------", itemName)
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
file, err := os.Open(currentPath)
|
file, err := os.Open(currentPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -347,14 +346,7 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
content, err := io.ReadAll(res.Body)
|
return consumeImageLoadResponse(res.Body, taskItem.Log)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if strings.Contains(string(content), "Error") {
|
|
||||||
return errors.New(string(content))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
@@ -362,6 +354,29 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker may report load failures in a successful HTTP response's JSON stream.
|
||||||
|
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
for {
|
||||||
|
var message jsonmessage.JSONMessage
|
||||||
|
if err := decoder.Decode(&message); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if message.Error != nil && message.Error.Message != "" {
|
||||||
|
return message.Error
|
||||||
|
}
|
||||||
|
if message.ErrorMessage != "" {
|
||||||
|
return errors.New(message.ErrorMessage)
|
||||||
|
}
|
||||||
|
if text := strings.TrimSpace(message.Stream); text != "" {
|
||||||
|
log(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mcpMysqlStoppedMessage = "The selected MySQL instance is not running. Start the database service and try the query again."
|
||||||
|
const mcpMysqlConnectionFailedMessage = "Cannot connect to the selected MySQL instance. Check the database service, connection settings and panel logs."
|
||||||
|
|
||||||
|
var errMCPMysqlStopped = mcpapi.NewBusinessError(mcpapi.CodeMySQLStopped, mcpMysqlStoppedMessage)
|
||||||
|
var errMCPMysqlConnectionFailed = mcpapi.NewBusinessError(mcpapi.CodeMySQLConnectionFailed, mcpMysqlConnectionFailedMessage)
|
||||||
|
|
||||||
|
var mcpMysqlStoppedPattern = regexp.MustCompile(`^Error response from daemon: [Cc]ontainer [A-Za-z0-9][A-Za-z0-9_.-]{0,127} is not running$`)
|
||||||
|
var mcpMysqlConnectionPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`^ERROR 2002 \(HY000\): Can't connect to local MySQL server through socket '[^'\r\n]{1,1000}' \([0-9]{1,10}\)$`),
|
||||||
|
regexp.MustCompile(`^ERROR 2003 \(HY000\): Can't connect to MySQL server on '[^'\r\n]{1,1000}' \([0-9]{1,10}\)$`),
|
||||||
|
regexp.MustCompile(`^ERROR 1045 \(28000\): Access denied for user '[^'\r\n]{1,128}'@'[^'\r\n]{1,255}' \(using password: (YES|NO)\)$`),
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyMCPMysqlReadFailure(stderr string) error {
|
||||||
|
generic := errors.New("local MySQL status query failed")
|
||||||
|
if len(stderr) == 0 || len(stderr) > 2048 || !utf8.ValidString(stderr) {
|
||||||
|
return generic
|
||||||
|
}
|
||||||
|
warning := "mysql: [Warning] Using a password on the command line interface can be insecure."
|
||||||
|
stderr = strings.TrimPrefix(stderr, warning+"\n")
|
||||||
|
if strings.HasSuffix(stderr, "\r\n") {
|
||||||
|
stderr = strings.TrimSuffix(stderr, "\r\n")
|
||||||
|
} else {
|
||||||
|
stderr = strings.TrimSuffix(stderr, "\n")
|
||||||
|
}
|
||||||
|
if strings.IndexFunc(stderr, unicode.IsControl) >= 0 {
|
||||||
|
return generic
|
||||||
|
}
|
||||||
|
if mcpMysqlStoppedPattern.MatchString(stderr) {
|
||||||
|
return errMCPMysqlStopped
|
||||||
|
}
|
||||||
|
for _, pattern := range mcpMysqlConnectionPatterns {
|
||||||
|
if pattern.MatchString(stderr) {
|
||||||
|
return errMCPMysqlConnectionFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return generic
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"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/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mcpMysqlQueryTimeout = 30 * time.Second
|
||||||
|
const mcpMysqlQueryOutputLimit = 1 << 20
|
||||||
|
|
||||||
|
func mcpLocalMysqlQuery(ctx context.Context) *gorm.DB {
|
||||||
|
return global.DB.WithContext(ctx).Table("databases AS d").
|
||||||
|
Joins("JOIN app_installs AS ai ON ai.id = d.app_install_id AND ai.name = d.name").
|
||||||
|
Joins("JOIN apps AS a ON a.id = ai.app_id").
|
||||||
|
Where("d.type = ? AND d.\"from\" = ? AND a.key = ?", constant.AppMysql, "local", constant.AppMysql).
|
||||||
|
Where("ai.status <> ? AND ai.container_name <> ?", constant.StatusDeleted, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseService) ListForMCP(ctx context.Context, dbType string) ([]dto.DatabaseOption, error) {
|
||||||
|
if dbType != constant.AppMysql {
|
||||||
|
return nil, errors.New("only installed local MySQL instances are supported")
|
||||||
|
}
|
||||||
|
result := make([]dto.DatabaseOption, 0)
|
||||||
|
err := mcpLocalMysqlQuery(ctx).Select("d.id, d.type, d.\"from\", d.name AS database, ai.version").Order("d.id").Scan(&result).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("cannot read local MySQL instances")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mcpMysqlReadRunner func(context.Context, string, string, string) (string, error)
|
||||||
|
|
||||||
|
func (u *MysqlService) LoadStatusForMCP(ctx context.Context, req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) {
|
||||||
|
return u.loadStatusForMCP(ctx, req, runMCPMysqlRead)
|
||||||
|
}
|
||||||
|
func (u *MysqlService) loadStatusForMCP(ctx context.Context, req dto.OperationWithNameAndType, run mcpMysqlReadRunner) (*dto.MysqlStatus, error) {
|
||||||
|
if req.Type != constant.AppMysql || req.Name == "" || len(req.Name) > 128 || strings.TrimSpace(req.Name) != req.Name || strings.ContainsAny(req.Name, "\x00\r\n/\\") || ctx.Err() != nil {
|
||||||
|
return nil, errors.New("an explicitly named installed local MySQL instance is required")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, mcpMysqlQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
var target struct {
|
||||||
|
ID uint
|
||||||
|
ContainerName, Version, Env string
|
||||||
|
}
|
||||||
|
if err := mcpLocalMysqlQuery(ctx).Where("d.name = ?", req.Name).Select("d.id, ai.container_name, ai.version, ai.env").Take(&target).Error; err != nil || target.ID == 0 {
|
||||||
|
return nil, errors.New("the installed local MySQL instance is unavailable")
|
||||||
|
}
|
||||||
|
var env map[string]any
|
||||||
|
if json.Unmarshal([]byte(target.Env), &env) != nil {
|
||||||
|
return nil, errors.New("local MySQL configuration is unavailable")
|
||||||
|
}
|
||||||
|
password, ok := env["PANEL_DB_ROOT_PASSWORD"].(string)
|
||||||
|
if !ok || password == "" {
|
||||||
|
return nil, errors.New("local MySQL configuration is unavailable")
|
||||||
|
}
|
||||||
|
app := &repo.RootInfo{ContainerName: target.ContainerName, Version: target.Version, Key: constant.AppMysql, Password: password}
|
||||||
|
rows := func(container, _, password, sql string) ([]string, error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, errors.New("local MySQL status query failed")
|
||||||
|
}
|
||||||
|
output, err := run(ctx, container, password, sql)
|
||||||
|
if err != nil {
|
||||||
|
if err == errMCPMysqlStopped || err == errMCPMysqlConnectionFailed {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, errors.New("local MySQL status query failed")
|
||||||
|
}
|
||||||
|
return strings.Split(output, "\n"), nil
|
||||||
|
}
|
||||||
|
maps := func(container, dbType, password, sql string) (map[string]string, error) {
|
||||||
|
lines, err := rows(container, dbType, password, sql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values := map[string]string{}
|
||||||
|
for _, line := range lines {
|
||||||
|
if pair := strings.Split(line, "\t"); len(pair) == 2 {
|
||||||
|
values[pair[0]] = pair[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
return loadMysqlStatus(req, app, maps, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mcpQueryOutput struct {
|
||||||
|
buffer bytes.Buffer
|
||||||
|
mu sync.Mutex
|
||||||
|
exceeded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *mcpQueryOutput) Write(data []byte) (int, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if len(data) > mcpMysqlQueryOutputLimit-b.buffer.Len() {
|
||||||
|
b.exceeded = true
|
||||||
|
return 0, errors.New("query output limit exceeded")
|
||||||
|
}
|
||||||
|
return b.buffer.Write(data)
|
||||||
|
}
|
||||||
|
func (b *mcpQueryOutput) Len() int {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buffer.Len()
|
||||||
|
}
|
||||||
|
func (b *mcpQueryOutput) String() string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buffer.String()
|
||||||
|
}
|
||||||
|
func runMCPMysqlRead(ctx context.Context, container, password, sql string) (string, error) {
|
||||||
|
switch sql {
|
||||||
|
case "show global status;", "show master status;", "show binary log status;":
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported MySQL monitoring query")
|
||||||
|
}
|
||||||
|
command := exec.CommandContext(ctx, "docker", "exec", container, "mysql", "-uroot", "-p"+password, "-e", sql)
|
||||||
|
output := &mcpQueryOutput{}
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
command.Stdout, command.Stderr = output, io.MultiWriter(output, &stderr)
|
||||||
|
runErr := command.Run()
|
||||||
|
if ctx.Err() != nil || output.exceeded {
|
||||||
|
return "", errors.New("local MySQL status query failed")
|
||||||
|
}
|
||||||
|
if runErr != nil {
|
||||||
|
return "", classifyMCPMysqlReadFailure(stderr.String())
|
||||||
|
}
|
||||||
|
value := strings.ReplaceAll(output.String(), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||||
|
if strings.HasPrefix(value, "ERROR ") {
|
||||||
|
return "", errors.New("local MySQL status query failed")
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppInstallService) GetAppInstallInfoForMCP(ctx context.Context, id uint) (*response.AppInstallInfo, error) {
|
||||||
|
var installed model.AppInstall
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("invalid installed application")
|
||||||
|
}
|
||||||
|
err := global.DB.WithContext(ctx).Preload("App").Where("id = ?", id).First(&installed).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &response.AppInstallInfo{Status: constant.StatusDeleted}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("installed application metadata is unavailable")
|
||||||
|
}
|
||||||
|
var env map[string]any
|
||||||
|
if json.Unmarshal([]byte(installed.Env), &env) != nil {
|
||||||
|
return nil, errors.New("installed application metadata is unavailable")
|
||||||
|
}
|
||||||
|
result := &response.AppInstallInfo{ID: installed.ID, Name: installed.Name, Version: installed.Version, Status: installed.Status, HttpPort: installed.HttpPort, AppKey: installed.App.Key, AppPorts: []int{}}
|
||||||
|
for key, value := range env {
|
||||||
|
if !strings.Contains(strings.ToUpper(key), "PANEL_APP_PORT") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var port int
|
||||||
|
switch item := value.(type) {
|
||||||
|
case string:
|
||||||
|
port, _ = strconv.Atoi(item)
|
||||||
|
case float64:
|
||||||
|
if item == float64(int(item)) {
|
||||||
|
port = int(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if port > 0 && port <= 65535 {
|
||||||
|
result.AppPorts = append(result.AppPorts, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Ints(result.AppPorts)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
+434
-135
@@ -33,9 +33,12 @@ import (
|
|||||||
type MonitorService struct {
|
type MonitorService struct {
|
||||||
DiskIO chan ([]disk.IOCountersStat)
|
DiskIO chan ([]disk.IOCountersStat)
|
||||||
NetIO chan ([]net.IOCountersStat)
|
NetIO chan ([]net.IOCountersStat)
|
||||||
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
monitorSettingMutex sync.Mutex
|
||||||
|
gpuMonitorMutex sync.Mutex
|
||||||
monitorCancel context.CancelFunc
|
monitorCancel context.CancelFunc
|
||||||
hostSysPath = loadHostSysPath()
|
hostSysPath = loadHostSysPath()
|
||||||
|
|
||||||
@@ -47,8 +50,13 @@ type IMonitorService interface {
|
|||||||
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
|
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
|
||||||
LoadSetting() (*dto.MonitorSetting, error)
|
LoadSetting() (*dto.MonitorSetting, error)
|
||||||
UpdateSetting(key, value string) error
|
UpdateSetting(key, value string) error
|
||||||
CleanData() error
|
CleanData(monitorType string) error
|
||||||
|
|
||||||
|
LoadIOOptions() []string
|
||||||
|
LoadNetworkOptions() []string
|
||||||
|
LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
|
||||||
|
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error)
|
||||||
|
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
|
||||||
LoadGPUOptions() dto.MonitorGPUOptions
|
LoadGPUOptions() dto.MonitorGPUOptions
|
||||||
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
|
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
|
||||||
|
|
||||||
@@ -60,6 +68,7 @@ func NewIMonitorService() IMonitorService {
|
|||||||
return &MonitorService{
|
return &MonitorService{
|
||||||
DiskIO: make(chan []disk.IOCountersStat, 2),
|
DiskIO: make(chan []disk.IOCountersStat, 2),
|
||||||
NetIO: make(chan []net.IOCountersStat, 2),
|
NetIO: make(chan []net.IOCountersStat, 2),
|
||||||
|
ctx: context.Background(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +94,7 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
|||||||
base.TopCPUItems = processes
|
base.TopCPUItems = processes
|
||||||
base.TopCPU = ""
|
base.TopCPU = ""
|
||||||
}
|
}
|
||||||
if req.Param == "all" || req.Param == "mem" {
|
if req.Param == "all" || req.Param == "memory" {
|
||||||
var processes []dto.Process
|
var processes []dto.Process
|
||||||
_ = json.Unmarshal([]byte(base.TopMem), &processes)
|
_ = json.Unmarshal([]byte(base.TopMem), &processes)
|
||||||
base.TopMemItems = processes
|
base.TopMemItems = processes
|
||||||
@@ -96,7 +105,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
|||||||
data = append(data, itemData)
|
data = append(data, itemData)
|
||||||
}
|
}
|
||||||
if req.Param == "all" || req.Param == "io" {
|
if req.Param == "all" || req.Param == "io" {
|
||||||
bases, err := monitorRepo.GetIO(repo.WithByName(req.IO), repo.WithByCreatedAt(req.StartTime, req.EndTime))
|
ioOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
|
||||||
|
if len(req.IO) != 0 {
|
||||||
|
ioOpts = append(ioOpts, repo.WithByName(req.IO))
|
||||||
|
}
|
||||||
|
bases, err := monitorRepo.GetIO(ioOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -110,7 +123,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
|||||||
data = append(data, itemData)
|
data = append(data, itemData)
|
||||||
}
|
}
|
||||||
if req.Param == "all" || req.Param == "network" {
|
if req.Param == "all" || req.Param == "network" {
|
||||||
bases, err := monitorRepo.GetNetwork(repo.WithByName(req.Network), repo.WithByCreatedAt(req.StartTime, req.EndTime))
|
netOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
|
||||||
|
if len(req.Network) != 0 {
|
||||||
|
netOpts = append(netOpts, repo.WithByName(req.Network))
|
||||||
|
}
|
||||||
|
bases, err := monitorRepo.GetNetwork(netOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -126,117 +143,171 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) LoadIOOptions() []string {
|
||||||
|
optionSet := make(map[string]struct{})
|
||||||
|
if diskStat, err := disk.IOCounters(); err == nil {
|
||||||
|
for _, item := range diskStat {
|
||||||
|
optionSet[item.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// union with names recorded in the monitor db so removed devices stay selectable
|
||||||
|
if names, err := monitorRepo.GetIONames(); err == nil {
|
||||||
|
for _, name := range names {
|
||||||
|
optionSet[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedMonitorOptions(optionSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) LoadNetworkOptions() []string {
|
||||||
|
optionSet := make(map[string]struct{})
|
||||||
|
if netStat, err := net.IOCounters(true); err == nil {
|
||||||
|
for _, item := range netStat {
|
||||||
|
optionSet[item.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if names, err := monitorRepo.GetNetworkNames(); err == nil {
|
||||||
|
for _, name := range names {
|
||||||
|
optionSet[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedMonitorOptions(optionSet)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
|
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
|
||||||
var data dto.MonitorGPUOptions
|
var data dto.MonitorGPUOptions
|
||||||
exist, client := accelerator.New()
|
exist, client := accelerator.New()
|
||||||
if !exist {
|
if !exist {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
snapshot, err := client.Collect(context.Background())
|
snapshot, err := client.Collect(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
|
global.LOG.Warnf("Load accelerator options failed: %v", err)
|
||||||
|
} else {
|
||||||
|
data = loadGPUOptions(snapshot)
|
||||||
|
for _, item := range data.ChartHide {
|
||||||
|
seen[item.DeviceID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.Supported = true
|
||||||
|
devices, err := monitorRepo.GetGPUDevices()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("Load accelerator history options failed: %v", err)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
if warning := snapshot.Warning(); warning != nil {
|
for _, device := range devices {
|
||||||
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
|
key := device.DeviceID
|
||||||
|
if key == "" {
|
||||||
|
key = "legacy:" + device.ProductName
|
||||||
}
|
}
|
||||||
return loadGPUOptions(snapshot)
|
if seen[key] {
|
||||||
}
|
continue
|
||||||
|
|
||||||
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
|
|
||||||
var data dto.MonitorGPUOptions
|
|
||||||
hasGPUOrNPU := false
|
|
||||||
hasXPU := false
|
|
||||||
for _, item := range snapshot.Devices {
|
|
||||||
if item.Kind == accelerator.KindXPU {
|
|
||||||
hasXPU = true
|
|
||||||
} else {
|
|
||||||
hasGPUOrNPU = true
|
|
||||||
}
|
}
|
||||||
}
|
seen[key] = true
|
||||||
switch {
|
data.ChartHide = append(data.ChartHide, dto.GPUChartHide{DeviceID: device.DeviceID, ProductName: device.ProductName, Type: device.DeviceType, Legacy: device.DeviceID == ""})
|
||||||
case hasGPUOrNPU && hasXPU:
|
data.Options = append(data.Options, device.ProductName)
|
||||||
data.GPUType = "mixed"
|
|
||||||
case hasXPU:
|
|
||||||
data.GPUType = "xpu"
|
|
||||||
case hasGPUOrNPU:
|
|
||||||
data.GPUType = "gpu"
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(snapshot.Devices, func(i, j int) bool {
|
|
||||||
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
|
|
||||||
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
|
|
||||||
}
|
|
||||||
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
|
|
||||||
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
|
|
||||||
}
|
|
||||||
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
|
|
||||||
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
|
|
||||||
}
|
|
||||||
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
|
|
||||||
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
|
|
||||||
}
|
|
||||||
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
|
|
||||||
})
|
|
||||||
for _, item := range snapshot.Devices {
|
|
||||||
optionType := "gpu"
|
|
||||||
if item.Kind == accelerator.KindXPU {
|
|
||||||
optionType = "xpu"
|
|
||||||
}
|
|
||||||
chartHide := dto.GPUChartHide{
|
|
||||||
ProductName: item.Label,
|
|
||||||
Type: optionType,
|
|
||||||
GPU: !item.Capabilities.Utilization,
|
|
||||||
Memory: !item.Capabilities.Memory,
|
|
||||||
Power: !item.Capabilities.Power,
|
|
||||||
PowerLimit: !item.Capabilities.PowerLimit,
|
|
||||||
Temperature: !item.Capabilities.Temperature,
|
|
||||||
Speed: !item.Capabilities.FanSpeed,
|
|
||||||
}
|
|
||||||
data.ChartHide = append(data.ChartHide, chartHide)
|
|
||||||
data.Options = append(data.Options, chartHide.ProductName)
|
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
|
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
|
||||||
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
|
||||||
req.StartTime = req.StartTime.In(loc)
|
|
||||||
req.EndTime = req.EndTime.In(loc)
|
|
||||||
var data dto.MonitorGPUData
|
var data dto.MonitorGPUData
|
||||||
gpuList, err := monitorRepo.GetGPU(repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByProductName(req.ProductName))
|
if req.StartTime.IsZero() || req.EndTime.IsZero() || !req.EndTime.After(req.StartTime) {
|
||||||
|
return data, fmt.Errorf("invalid GPU history time range")
|
||||||
|
}
|
||||||
|
if req.DeviceID == "" && req.ProductName == "" {
|
||||||
|
return data, fmt.Errorf("GPU history requires a device")
|
||||||
|
}
|
||||||
|
if req.Aggregation != "" && req.Aggregation != "avg" && req.Aggregation != "max" {
|
||||||
|
return data, fmt.Errorf("invalid GPU history aggregation")
|
||||||
|
}
|
||||||
|
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
||||||
for _, gpu := range gpuList {
|
opts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByGPUDevice(req.DeviceID, req.ProductName, req.Legacy)}
|
||||||
data.Date = append(data.Date, gpu.CreatedAt)
|
data.SampleCount, err = monitorRepo.CountGPU(opts...)
|
||||||
data.GPUValue = append(data.GPUValue, gpu.GPUUtil)
|
if err != nil || data.SampleCount == 0 {
|
||||||
data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature)
|
return data, err
|
||||||
data.PowerUsed = append(data.PowerUsed, gpu.PowerDraw)
|
|
||||||
data.PowerTotal = append(data.PowerTotal, gpu.MaxPowerLimit)
|
|
||||||
if gpu.MaxPowerLimit != 0 {
|
|
||||||
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100)
|
|
||||||
} else {
|
|
||||||
data.PowerPercent = append(data.PowerPercent, float64(0))
|
|
||||||
}
|
}
|
||||||
|
if data.SampleCount > 1200 {
|
||||||
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal)
|
seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
|
||||||
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed)
|
data.BucketSeconds = (seconds + 599) / 600
|
||||||
if gpu.MemTotal != 0 {
|
|
||||||
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100)
|
|
||||||
} else {
|
|
||||||
data.MemoryPercent = append(data.MemoryPercent, float64(0))
|
|
||||||
}
|
}
|
||||||
var process []dto.GPUProcess
|
points, err := monitorRepo.GetGPUHistory(req.StartTime, data.BucketSeconds, req.Aggregation, opts...)
|
||||||
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil {
|
if err != nil {
|
||||||
data.ProcessCount = append(data.ProcessCount, len(process))
|
return data, err
|
||||||
data.GPUProcesses = append(data.GPUProcesses, process)
|
|
||||||
} else {
|
|
||||||
data.ProcessCount = append(data.ProcessCount, 0)
|
|
||||||
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{})
|
|
||||||
}
|
}
|
||||||
data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed)
|
samples := make([]repo.GPUHistoryPoint, 0, len(points))
|
||||||
|
if data.BucketSeconds > 0 {
|
||||||
|
next := 0
|
||||||
|
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
|
||||||
|
point := repo.GPUHistoryPoint{}
|
||||||
|
if next < len(points) && points[next].Bucket == bucket {
|
||||||
|
point = points[next]
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
|
||||||
|
if bucket == 0 {
|
||||||
|
point.CreatedAt = req.StartTime
|
||||||
|
}
|
||||||
|
samples = append(samples, point)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i, point := range points {
|
||||||
|
if i > 0 && points[i-1].IntervalSeconds > 0 {
|
||||||
|
interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
|
||||||
|
if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
||||||
|
samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
samples = append(samples, point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, point := range samples {
|
||||||
|
data.Date = append(data.Date, point.CreatedAt)
|
||||||
|
data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
|
||||||
|
data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
|
||||||
|
data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
|
||||||
|
data.JPEGUtil = append(data.JPEGUtil, point.JPEGUtil)
|
||||||
|
data.OFAUtil = append(data.OFAUtil, point.OFAUtil)
|
||||||
|
data.MediaUtil = append(data.MediaUtil, point.MediaUtil)
|
||||||
|
data.ComputeUtil = append(data.ComputeUtil, point.ComputeUtil)
|
||||||
|
data.CopyUtil = append(data.CopyUtil, point.CopyUtil)
|
||||||
|
data.HotspotTemperature = append(data.HotspotTemperature, point.HotspotTemperature)
|
||||||
|
data.FanRPM = append(data.FanRPM, point.FanRPM)
|
||||||
|
data.AICPUUtil = append(data.AICPUUtil, point.AICPUUtil)
|
||||||
|
data.CtrlCPUUtil = append(data.CtrlCPUUtil, point.CtrlCPUUtil)
|
||||||
|
data.DDRUsed = append(data.DDRUsed, point.DDRUsed)
|
||||||
|
data.DDRTotal = append(data.DDRTotal, point.DDRTotal)
|
||||||
|
data.HBMUsed = append(data.HBMUsed, point.HBMUsed)
|
||||||
|
data.HBMTotal = append(data.HBMTotal, point.HBMTotal)
|
||||||
|
data.DDRBandwidth = append(data.DDRBandwidth, point.DDRBandwidth)
|
||||||
|
data.HBMBandwidth = append(data.HBMBandwidth, point.HBMBandwidth)
|
||||||
|
data.MemoryBandwidth = append(data.MemoryBandwidth, point.MemoryBandwidth)
|
||||||
|
data.MediaFrequency = append(data.MediaFrequency, point.MediaFrequency)
|
||||||
|
data.HugepagesUsed = append(data.HugepagesUsed, point.HugepagesUsed)
|
||||||
|
data.HugepagesTotal = append(data.HugepagesTotal, point.HugepagesTotal)
|
||||||
|
data.GPUValue = append(data.GPUValue, point.GPUUtil)
|
||||||
|
data.TemperatureValue = append(data.TemperatureValue, point.Temperature)
|
||||||
|
data.MemoryTemperatureValue = append(data.MemoryTemperatureValue, point.MemoryTemperature)
|
||||||
|
data.PowerUsed = append(data.PowerUsed, point.PowerDraw)
|
||||||
|
data.PowerTotal = append(data.PowerTotal, point.MaxPowerLimit)
|
||||||
|
data.PowerPercent = append(data.PowerPercent, point.PowerPercent)
|
||||||
|
data.MemoryPercent = append(data.MemoryPercent, point.MemoryPercent)
|
||||||
|
data.MemoryTotal = append(data.MemoryTotal, point.MemTotal)
|
||||||
|
data.MemoryUsed = append(data.MemoryUsed, point.MemUsed)
|
||||||
|
data.SpeedValue = append(data.SpeedValue, point.FanSpeed)
|
||||||
|
data.FrequencyValue = append(data.FrequencyValue, point.Frequency)
|
||||||
|
data.MemoryFrequencyValue = append(data.MemoryFrequencyValue, point.MemoryFrequency)
|
||||||
|
data.ProcessCount = append(data.ProcessCount, point.ProcessCount)
|
||||||
|
var processes []dto.GPUProcess
|
||||||
|
if data.BucketSeconds == 0 && point.ProcessCount != nil {
|
||||||
|
_ = json.Unmarshal([]byte(point.Processes), &processes)
|
||||||
|
}
|
||||||
|
data.GPUProcesses = append(data.GPUProcesses, processes)
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@@ -262,9 +333,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) UpdateSetting(key, value string) error {
|
func (m *MonitorService) UpdateSetting(key, value string) error {
|
||||||
|
monitorSettingMutex.Lock()
|
||||||
|
defer monitorSettingMutex.Unlock()
|
||||||
|
switch key {
|
||||||
|
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
|
||||||
|
if value != constant.StatusEnable && value != constant.StatusDisable {
|
||||||
|
return fmt.Errorf("invalid monitoring status")
|
||||||
|
}
|
||||||
|
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
|
||||||
|
interval, err := strconv.Atoi(value)
|
||||||
|
if err != nil || interval < 10 || interval > 43200 {
|
||||||
|
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
|
||||||
|
}
|
||||||
|
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
|
||||||
|
days, err := strconv.Atoi(value)
|
||||||
|
if err != nil || days < 1 {
|
||||||
|
return fmt.Errorf("monitoring retention must be a positive integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
switch key {
|
switch key {
|
||||||
case "MonitorStatus":
|
case "MonitorStatus":
|
||||||
if value == constant.StatusEnable && global.MonitorCronID == 0 {
|
if value == constant.StatusEnable && monitorCancel == nil {
|
||||||
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
|
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -273,8 +362,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value == constant.StatusDisable && global.MonitorCronID != 0 {
|
if value == constant.StatusDisable && monitorCancel != nil {
|
||||||
monitorCancel()
|
monitorCancel()
|
||||||
|
monitorCancel = nil
|
||||||
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
||||||
global.MonitorCronID = 0
|
global.MonitorCronID = 0
|
||||||
}
|
}
|
||||||
@@ -283,31 +373,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
|
if status.Value == constant.StatusEnable && monitorCancel != nil {
|
||||||
if err := StartMonitor(true, value); err != nil {
|
if err := StartMonitor(true, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "GPUMonitorStatus":
|
||||||
|
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
|
||||||
|
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := StartGPUMonitor(interval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
|
||||||
|
global.Cron.Remove(global.GPUMonitorCronID)
|
||||||
|
global.GPUMonitorCronID = 0
|
||||||
|
}
|
||||||
|
case "GPUMonitorInterval":
|
||||||
|
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status == constant.StatusEnable {
|
||||||
|
if err := StartGPUMonitor(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "VLLMMonitorStatus":
|
||||||
|
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
|
||||||
|
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := StartVLLMMonitor(interval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
|
||||||
|
global.Cron.Remove(global.VLLMMonitorCronID)
|
||||||
|
global.VLLMMonitorCronID = 0
|
||||||
|
}
|
||||||
|
case "VLLMMonitorInterval":
|
||||||
|
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status == constant.StatusEnable {
|
||||||
|
if err := StartVLLMMonitor(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return settingRepo.Update(key, value)
|
return settingRepo.Update(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) CleanData() error {
|
func (m *MonitorService) CleanData(monitorType string) error {
|
||||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
|
switch monitorType {
|
||||||
return err
|
case "host":
|
||||||
|
return monitorRepo.CleanHost()
|
||||||
|
case "gpu":
|
||||||
|
gpuMonitorMutex.Lock()
|
||||||
|
defer gpuMonitorMutex.Unlock()
|
||||||
|
return monitorRepo.CleanGPU()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
|
||||||
}
|
}
|
||||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) Run() {
|
func (m *MonitorService) Run() {
|
||||||
saveAcceleratorDataToDB()
|
if m.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
var itemModel model.MonitorBase
|
var itemModel model.MonitorBase
|
||||||
totalPercent, _ := cpu.Percent(3*time.Second, false)
|
totalPercent, _ := cpu.Percent(3*time.Second, false)
|
||||||
if len(totalPercent) == 1 {
|
if len(totalPercent) == 1 {
|
||||||
@@ -353,30 +492,34 @@ func (m *MonitorService) Run() {
|
|||||||
_ = monitorRepo.DelMonitorBase(timeForDelete)
|
_ = monitorRepo.DelMonitorBase(timeForDelete)
|
||||||
_ = monitorRepo.DelMonitorIO(timeForDelete)
|
_ = monitorRepo.DelMonitorIO(timeForDelete)
|
||||||
_ = monitorRepo.DelMonitorNet(timeForDelete)
|
_ = monitorRepo.DelMonitorNet(timeForDelete)
|
||||||
_ = monitorRepo.DelMonitorGPU(timeForDelete)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) loadDiskIO() {
|
func (m *MonitorService) loadDiskIO() {
|
||||||
ioStat, _ := disk.IOCounters()
|
ioStat, _ := disk.IOCountersWithContext(m.ctx)
|
||||||
var diskIOList []disk.IOCountersStat
|
var diskIOList []disk.IOCountersStat
|
||||||
for _, io := range ioStat {
|
for _, io := range ioStat {
|
||||||
diskIOList = append(diskIOList, io)
|
diskIOList = append(diskIOList, io)
|
||||||
}
|
}
|
||||||
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
|
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
|
||||||
m.DiskIO <- diskIOList
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
case m.DiskIO <- diskIOList:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) loadNetIO() {
|
func (m *MonitorService) loadNetIO() {
|
||||||
netStat, _ := net.IOCounters(true)
|
netStat, _ := net.IOCountersWithContext(m.ctx, true)
|
||||||
netStatAll, _ := net.IOCounters(false)
|
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
|
||||||
var netList []net.IOCountersStat
|
var netList []net.IOCountersStat
|
||||||
netList = append(netList, netStat...)
|
netList = append(netList, netStat...)
|
||||||
netList = append(netList, netStatAll...)
|
netList = append(netList, netStatAll...)
|
||||||
m.NetIO <- netList
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
case m.NetIO <- netList:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
|
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
|
||||||
defer close(m.DiskIO)
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -426,14 +569,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = monitorRepo.BatchCreateMonitorIO(ioList)
|
_ = monitorRepo.BatchCreateMonitorIO(ioList)
|
||||||
m.DiskIO <- ioStat2
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case m.DiskIO <- ioStat2:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
|
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
|
||||||
defer close(m.NetIO)
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -463,7 +609,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = monitorRepo.BatchCreateMonitorNet(netList)
|
_ = monitorRepo.BatchCreateMonitorNet(netList)
|
||||||
m.NetIO <- netStat2
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case m.NetIO <- netStat2:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -572,27 +722,38 @@ func loadTopMem() []dto.Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartMonitor(removeBefore bool, interval string) error {
|
func StartMonitor(removeBefore bool, interval string) error {
|
||||||
|
intervalItem, err := strconv.Atoi(interval)
|
||||||
|
if err != nil || intervalItem < 10 || intervalItem > 43200 {
|
||||||
|
return fmt.Errorf("invalid host monitoring interval: %s", interval)
|
||||||
|
}
|
||||||
if removeBefore {
|
if removeBefore {
|
||||||
monitorCancel()
|
monitorCancel()
|
||||||
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
||||||
}
|
}
|
||||||
intervalItem, err := strconv.Atoi(interval)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
service := NewIMonitorService()
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
monitorCancel = cancel
|
service := &MonitorService{
|
||||||
|
DiskIO: make(chan []disk.IOCountersStat, 2),
|
||||||
|
NetIO: make(chan []net.IOCountersStat, 2),
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
|
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
|
||||||
time.AfterFunc(time.Until(nextMinute), func() {
|
timer := time.AfterFunc(time.Until(nextMinute), func() {
|
||||||
|
monitorSettingMutex.Lock()
|
||||||
|
defer monitorSettingMutex.Unlock()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
|
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
global.MonitorCronID = monitorID
|
global.MonitorCronID = monitorID
|
||||||
})
|
})
|
||||||
|
monitorCancel = func() {
|
||||||
|
cancel()
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
service.Run()
|
service.Run()
|
||||||
|
|
||||||
@@ -602,7 +763,97 @@ func StartMonitor(removeBefore bool, interval string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveAcceleratorDataToDB() {
|
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
|
||||||
|
var data dto.MonitorGPUOptions
|
||||||
|
hasGPUOrNPU := false
|
||||||
|
hasXPU := false
|
||||||
|
for _, item := range snapshot.Devices {
|
||||||
|
if item.Kind == accelerator.KindXPU {
|
||||||
|
hasXPU = true
|
||||||
|
} else {
|
||||||
|
hasGPUOrNPU = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case hasGPUOrNPU && hasXPU:
|
||||||
|
data.GPUType = "mixed"
|
||||||
|
case hasXPU:
|
||||||
|
data.GPUType = "xpu"
|
||||||
|
case hasGPUOrNPU:
|
||||||
|
data.GPUType = "gpu"
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(snapshot.Devices, func(i, j int) bool {
|
||||||
|
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
|
||||||
|
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
|
||||||
|
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
|
||||||
|
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
|
||||||
|
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
|
||||||
|
}
|
||||||
|
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
|
||||||
|
})
|
||||||
|
for _, item := range snapshot.Devices {
|
||||||
|
chartHide := dto.GPUChartHide{
|
||||||
|
DeviceID: item.ID,
|
||||||
|
ProductName: item.Label,
|
||||||
|
Type: string(item.Kind),
|
||||||
|
}
|
||||||
|
data.ChartHide = append(data.ChartHide, chartHide)
|
||||||
|
data.Options = append(data.Options, chartHide.ProductName)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartGPUMonitor(interval string) error {
|
||||||
|
seconds, err := strconv.Atoi(interval)
|
||||||
|
if err != nil || seconds < 10 || seconds > 43200 {
|
||||||
|
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
|
||||||
|
}
|
||||||
|
service := &MonitorService{}
|
||||||
|
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
|
||||||
|
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if global.GPUMonitorCronID != 0 {
|
||||||
|
global.Cron.Remove(global.GPUMonitorCronID)
|
||||||
|
}
|
||||||
|
global.GPUMonitorCronID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) saveGPUData() {
|
||||||
|
if !gpuMonitorMutex.TryLock() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer gpuMonitorMutex.Unlock()
|
||||||
|
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != constant.StatusEnable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
days, err := strconv.Atoi(retention)
|
||||||
|
if err != nil || days < 1 {
|
||||||
|
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
|
||||||
|
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
|
||||||
|
}
|
||||||
exist, client := accelerator.New()
|
exist, client := accelerator.New()
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return
|
||||||
@@ -615,27 +866,64 @@ func saveAcceleratorDataToDB() {
|
|||||||
if warning := snapshot.Warning(); warning != nil {
|
if warning := snapshot.Warning(); warning != nil {
|
||||||
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
|
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
|
||||||
}
|
}
|
||||||
|
intervalSeconds := 0
|
||||||
|
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
|
||||||
|
intervalSeconds, _ = strconv.Atoi(setting.Value)
|
||||||
|
}
|
||||||
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
|
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
|
||||||
for _, device := range snapshot.Devices {
|
for _, device := range snapshot.Devices {
|
||||||
list = append(list, newMonitorGPU(device))
|
item := newMonitorGPU(device)
|
||||||
|
item.CreatedAt = snapshot.Info.CollectedAt
|
||||||
|
item.IntervalSeconds = intervalSeconds
|
||||||
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
|
if err := monitorRepo.BatchCreateMonitorGPU(list); err != nil {
|
||||||
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
|
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
|
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
|
||||||
item := model.MonitorGPU{
|
item := model.MonitorGPU{
|
||||||
|
MemoryUtil: device.Metrics.MemoryUtil.Value,
|
||||||
|
MemoryActivity: device.Metrics.MemoryActivity.Value,
|
||||||
|
EncoderUtil: device.Metrics.EncoderUtil.Value,
|
||||||
|
DecoderUtil: device.Metrics.DecoderUtil.Value,
|
||||||
|
JPEGUtil: device.Metrics.JPEGUtil.Value,
|
||||||
|
OFAUtil: device.Metrics.OFAUtil.Value,
|
||||||
|
MediaUtil: device.Metrics.MediaUtil.Value,
|
||||||
|
ComputeUtil: device.Metrics.ComputeUtil.Value,
|
||||||
|
CopyUtil: device.Metrics.CopyUtil.Value,
|
||||||
|
HotspotTemperature: device.Metrics.HotspotTemperature.Value,
|
||||||
|
FanRPM: device.Metrics.FanRPM.Value,
|
||||||
|
AICPUUtil: device.Metrics.AICPUUtil.Value,
|
||||||
|
CtrlCPUUtil: device.Metrics.CtrlCPUUtil.Value,
|
||||||
|
DDRUsed: device.Metrics.DDRUsed.Value,
|
||||||
|
DDRTotal: device.Metrics.DDRTotal.Value,
|
||||||
|
HBMUsed: device.Metrics.HBMUsed.Value,
|
||||||
|
HBMTotal: device.Metrics.HBMTotal.Value,
|
||||||
|
DDRBandwidth: device.Metrics.DDRBandwidth.Value,
|
||||||
|
HBMBandwidth: device.Metrics.HBMBandwidth.Value,
|
||||||
|
MemoryBandwidth: device.Metrics.MemoryBandwidth.Value,
|
||||||
|
MediaFrequency: device.Metrics.MediaFrequency.Value,
|
||||||
|
HugepagesUsed: device.Metrics.HugepagesUsed.Value,
|
||||||
|
HugepagesTotal: device.Metrics.HugepagesTotal.Value,
|
||||||
|
|
||||||
ProductName: device.Label,
|
ProductName: device.Label,
|
||||||
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
|
DeviceID: device.ID,
|
||||||
Temperature: device.Metrics.Temperature.ValueOrZero(),
|
DeviceType: string(device.Kind),
|
||||||
PowerDraw: device.Metrics.Power.ValueOrZero(),
|
ProcessStatus: device.ProcessStatus,
|
||||||
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
|
Frequency: device.Metrics.Frequency.Value,
|
||||||
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
|
MemoryFrequency: device.Metrics.MemoryFrequency.Value,
|
||||||
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
|
MemoryTemperature: device.Metrics.MemoryTemperature.Value,
|
||||||
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
|
GPUUtil: device.Metrics.Utilization.Value,
|
||||||
|
Temperature: device.Metrics.Temperature.Value,
|
||||||
|
PowerDraw: device.Metrics.Power.Value,
|
||||||
|
MaxPowerLimit: device.Metrics.PowerLimit.Value,
|
||||||
|
MemUsed: device.Metrics.MemoryUsed.Value,
|
||||||
|
MemTotal: device.Metrics.MemoryTotal.Value,
|
||||||
|
FanSpeed: device.Metrics.FanSpeed.Value,
|
||||||
}
|
}
|
||||||
if len(device.Processes) == 0 {
|
if device.ProcessStatus != "ok" {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
processes := make([]dto.GPUProcess, 0, len(device.Processes))
|
processes := make([]dto.GPUProcess, 0, len(device.Processes))
|
||||||
@@ -695,3 +983,14 @@ func loadHostSysPath() string {
|
|||||||
}
|
}
|
||||||
return hostSys
|
return hostSys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortedMonitorOptions(optionSet map[string]struct{}) []string {
|
||||||
|
options := make([]string, 0, len(optionSet))
|
||||||
|
for name := range optionSet {
|
||||||
|
if len(name) != 0 && name != "all" {
|
||||||
|
options = append(options, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(options)
|
||||||
|
return append([]string{"all"}, options...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
var vllmMonitorMutex sync.Mutex
|
||||||
|
var vllmMetricsClient = &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
|
||||||
|
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
|
||||||
|
if !req.EndTime.After(req.StartTime) {
|
||||||
|
return data, fmt.Errorf("invalid vLLM history request")
|
||||||
|
}
|
||||||
|
install, err := appInstallRepo.GetFirstByCtx(ctx, repo.WithByID(req.AppInstallID))
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if install.App.Key != "vllm" {
|
||||||
|
return data, fmt.Errorf("not a vLLM installation")
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
||||||
|
data.SampleCount, err = vllmMonitorRepo.Count(ctx, req.AppInstallID, req.StartTime, req.EndTime)
|
||||||
|
if err != nil || data.SampleCount == 0 {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if data.SampleCount > 1200 {
|
||||||
|
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
|
||||||
|
}
|
||||||
|
points, err := vllmMonitorRepo.History(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group)
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
for i := range points {
|
||||||
|
points[i].MonitorVLLM.CompletedRequests = points[i].CompletedRequests
|
||||||
|
}
|
||||||
|
if data.BucketSeconds > 0 {
|
||||||
|
aggregates := map[int64]*vllm.HistoryAccumulator{}
|
||||||
|
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
|
||||||
|
if aggregates[bucket] == nil {
|
||||||
|
aggregates[bucket] = &vllm.HistoryAccumulator{}
|
||||||
|
}
|
||||||
|
return aggregates[bucket].Add(histograms, requests, req.Aggregation, req.Group)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
for i := range points {
|
||||||
|
if aggregate := aggregates[points[i].Bucket]; aggregate != nil {
|
||||||
|
aggregate.Apply(&points[i].MonitorVLLM, req.Aggregation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next := 0
|
||||||
|
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
|
||||||
|
var point model.MonitorVLLM
|
||||||
|
if next < len(points) && points[next].Bucket == bucket {
|
||||||
|
point = points[next].MonitorVLLM
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
point.AppInstallID = req.AppInstallID
|
||||||
|
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
|
||||||
|
if bucket == 0 {
|
||||||
|
point.CreatedAt = req.StartTime
|
||||||
|
}
|
||||||
|
data.Points = append(data.Points, point)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i, point := range points {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
var aggregate vllm.HistoryAccumulator
|
||||||
|
if err := aggregate.Add(point.HistogramDeltas, point.RequestDeltas, "max", req.Group); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
aggregate.Apply(&point.MonitorVLLM, "max")
|
||||||
|
if i > 0 {
|
||||||
|
previous := points[i-1]
|
||||||
|
interval := time.Duration(max(previous.SampleIntervalSeconds, point.SampleIntervalSeconds)) * time.Second
|
||||||
|
if interval == 0 {
|
||||||
|
if i > 1 {
|
||||||
|
interval = previous.CreatedAt.Sub(points[i-2].CreatedAt)
|
||||||
|
}
|
||||||
|
if i+1 < len(points) {
|
||||||
|
interval = max(interval, points[i+1].CreatedAt.Sub(point.CreatedAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if interval > 0 && point.CreatedAt.Sub(previous.CreatedAt) > 2*interval {
|
||||||
|
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: previous.CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.Points = append(data.Points, point.MonitorVLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error) {
|
||||||
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||||
|
if err != nil {
|
||||||
|
return dto.MonitorVLLMCurrentData{}, err
|
||||||
|
}
|
||||||
|
if install.App.Key != "vllm" {
|
||||||
|
return dto.MonitorVLLMCurrentData{}, fmt.Errorf("not a vLLM installation")
|
||||||
|
}
|
||||||
|
point, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
|
||||||
|
data := dto.MonitorVLLMCurrentData{MonitorVLLM: point}
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(point.RawMetrics), &data.Metrics); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
for name := range data.Metrics {
|
||||||
|
if name == "request_prompt_tokens_sum" || name == "request_prompt_tokens_count" ||
|
||||||
|
(!strings.HasSuffix(name, "_total") && !strings.HasSuffix(name, "_sum") && !strings.HasSuffix(name, "_count")) {
|
||||||
|
delete(data.Metrics, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
|
||||||
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.App.Key != "vllm" {
|
||||||
|
return fmt.Errorf("not a vLLM installation")
|
||||||
|
}
|
||||||
|
vllmMonitorMutex.Lock()
|
||||||
|
defer vllmMonitorMutex.Unlock()
|
||||||
|
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartVLLMMonitor(interval string) error {
|
||||||
|
seconds, err := strconv.Atoi(interval)
|
||||||
|
if err != nil || seconds < 10 || seconds > 43200 {
|
||||||
|
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
|
||||||
|
}
|
||||||
|
service := &MonitorService{}
|
||||||
|
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(func() { service.saveVLLMData(seconds) }))
|
||||||
|
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if global.VLLMMonitorCronID != 0 {
|
||||||
|
global.Cron.Remove(global.VLLMMonitorCronID)
|
||||||
|
}
|
||||||
|
global.VLLMMonitorCronID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MonitorService) saveVLLMData(intervalSeconds int) {
|
||||||
|
if !vllmMonitorMutex.TryLock() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer vllmMonitorMutex.Unlock()
|
||||||
|
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != constant.StatusEnable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
days, err := strconv.Atoi(retention)
|
||||||
|
if err != nil || days < 1 {
|
||||||
|
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
|
||||||
|
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
|
||||||
|
}
|
||||||
|
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(apps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids := make([]uint, 0, len(apps))
|
||||||
|
for _, app := range apps {
|
||||||
|
ids = append(ids, app.ID)
|
||||||
|
}
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var group errgroup.Group
|
||||||
|
group.SetLimit(4)
|
||||||
|
for _, install := range installs {
|
||||||
|
group.Go(func() error {
|
||||||
|
previous, err := vllmMonitorRepo.Latest(install.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
point, err := collectVLLMMetrics(context.Background(), install, previous)
|
||||||
|
point.SampleIntervalSeconds = intervalSeconds
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
|
||||||
|
}
|
||||||
|
return vllmMonitorRepo.Create(&point)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := group.Wait(); err != nil {
|
||||||
|
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
|
||||||
|
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
|
||||||
|
if install.HttpPort <= 0 || install.HttpPort > 65535 {
|
||||||
|
return point, fmt.Errorf("invalid vLLM port")
|
||||||
|
}
|
||||||
|
var env map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
host, _ := env[constant.HostIP].(string)
|
||||||
|
host = strings.Trim(host, "[]")
|
||||||
|
switch host {
|
||||||
|
case "", "0.0.0.0":
|
||||||
|
host = "127.0.0.1"
|
||||||
|
case "::":
|
||||||
|
host = "::1"
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
response, err := vllmMetricsClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
if len(body) > 8*1024*1024 {
|
||||||
|
return point, fmt.Errorf("vLLM metrics response too large")
|
||||||
|
}
|
||||||
|
metrics, err := vllm.Parse(bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
point.CreatedAt = time.Now()
|
||||||
|
raw, err := json.Marshal(metrics)
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
var before vllm.Metrics
|
||||||
|
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
|
||||||
|
if previous.Status == "ok" && elapsed > 0 {
|
||||||
|
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calculated, err := vllm.Calculate(metrics, before, elapsed)
|
||||||
|
if err != nil {
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
calculated.AppInstallID = install.ID
|
||||||
|
calculated.CreatedAt = point.CreatedAt
|
||||||
|
calculated.RawMetrics = string(raw)
|
||||||
|
calculated.Status = "ok"
|
||||||
|
return calculated, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user