Compare commits

..
Author SHA1 Message Date
guoyuqi 200ddbfa28 feat: redis sync and image upload 2026-09-30 17:37:33 +08:00
guoyuqi f34418da6d refactor: select bind 2026-09-29 17:11:06 +08:00
guoyuqi 7ff86c0ee3 Merge remote-tracking branch 'origin/dev-v2' into pr@dev-v2@fix-cleanDockerNetwork 2026-09-29 11:09:13 +08:00
guoyuqi 5a58f44548 refactor: search bind 2026-09-29 11:00:30 +08:00
ssongliu d27d6db1ea feat: enhance native GPU NPU and XPU monitoring (#13939) 2026-09-28 18:32:12 +08:00
guoyuqi c75cb5d7ce fix: fix log text 2026-09-28 14:06:40 +08:00
guoyuqi 2207147e7f fix: fix operation log 2026-09-28 11:03:33 +08:00
蘭 d2bb3813d9 fix: enhance SSH login alert by adding local IP addresses to success login whitelist (#13920) 2026-09-24 18:23:59 +08:00
蘭 cac73e6d45 fix: add success alert message for cron jobs and enhance alert handling (#13919) 2026-09-24 18:23:39 +08:00
guoyuqi b5604eab11 fix: fix container list 2026-09-24 15:06:32 +08:00
guoyuqi b498882708 Merge remote-tracking branch 'origin/dev-v2' into pr@dev-v2@fix-cleanDockerNetwork 2026-09-24 14:09:56 +08:00
ssongliu 4712ceaf2c fix: allow read-only firewall APIs in demo mode (#13914) 2026-09-24 14:01:05 +08:00
ssongliu 65243c68c4 fix: allow licensed community restore progress route (#13913) 2026-09-24 10:18:01 +08:00
guoyuqi 381d71663c Merge remote-tracking branch 'origin/pr@dev-v2@fix-cleanDockerNetwork' into pr@dev-v2@fix-cleanDockerNetwork
# Conflicts:
#	agent/app/api/v2/container.go
#	agent/app/dto/container_network_cleanup.go
#	agent/app/service/container_network_cleanup.go
#	agent/utils/docker/network_cleanup.go
2026-09-24 09:43:10 +08:00
guoyuqi f7f4135193 fix: fix clean network 2026-09-24 09:41:59 +08:00
ssongliu 256e79ca81 fix: remove minimum upgrade backup copies limit (#13907) 2026-09-23 18:17:32 +08:00
guoyuqi dbf6d9e5fe fix: fix clean network 2026-09-23 17:54:15 +08:00
ssongliu 4861eb69cb fix: separate firewalld batches by native option (#13896) 2026-09-22 18:20:18 +08:00
蘭 fb8cf15537 fix: Fix the automatic renewal certificate alert issue (#13895) 2026-09-22 15:06:48 +08:00
ssongliu c4a6791271 fix: allow updated SSH ports through firewall before restart (#13894) 2026-09-22 14:51:54 +08:00
蘭 f58e147636 fix: enhance file rename functionality (#13891) 2026-09-22 12:49:05 +08:00
ssongliu 387e9fbeed fix: correct firewall port allowances and whitelist sync reporting (#13892) 2026-09-22 12:48:40 +08:00
ssongliu 4eb627bc79 fix(vm): display additional virtual machine states (#13890) 2026-09-22 10:27:49 +08:00
ssongliu 850c86229c fix: log dashboard disk command failures (#13889) 2026-09-22 10:16:54 +08:00
CityFun f984917a66 fix(upgrade): resolve application upgrade failures (#13888) 2026-09-22 10:11:13 +08:00
A_Words 8588217fbf fix: prioritize HTTP-to-HTTPS redirects over website redirects (#13847) 2026-09-22 10:05:53 +08:00
ssongliu a2307c5f64 fix(firewall): improve batch operations and task log completion (#13886) 2026-09-22 09:46:46 +08:00
蘭 5923290de8 ref: add file task messages for copy, move, compress and decompress (#13885) 2026-09-21 17:32:14 +08:00
蘭 1c994fba4a ref: update documentation URL for the panel (#13884) 2026-09-21 17:28:23 +08:00
蘭 415ab96aab ref:improve the panel API key (#13883) 2026-09-21 17:18:15 +08:00
ssongliu 19bb823b05 fix(firewall): guard IPv6 forwarding against RA disruption (#13882) 2026-09-21 17:12:17 +08:00
ssongliu 3a5371652e refactor(firewall): simplify state reads and batch verification (#13881) 2026-09-21 15:09:13 +08:00
蘭 a267b4148a ref:Optimize and improve the panel API key (#13880) 2026-09-21 12:49:45 +08:00
蘭 36a01eb60d feat: The panel API key supports creating multiple tokens and enhance terminal capabilities (#13872) 2026-09-20 18:32:54 +08:00
王贺 65f6fdd045 fix(ssh): display SSH login logs in server timezone (#13869)
The SSH log page formatted dates with the browser timezone, so logs looked shifted when the client timezone differed from the server. Format the timestamp with the offset returned by the API so the page matches auth.log, the terminal and the CSV export. Refs #13860.
2026-09-20 18:02:46 +08:00
287 changed files with 17520 additions and 10997 deletions
+18 -3
View File
@@ -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
@@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
containerService.StreamLogs(c, streamLog) containerService.StreamLogs(c, streamLog)
} }
func (b *BaseApi) CleanNetworks(c *gin.Context) {
result, err := containerService.CleanNetworks()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+24 -3
View File
@@ -86,17 +86,38 @@ 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 {object} dto.RedisCliStatus
// @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
// @Summary Load redis-cli installation status
// @Success 200 {object} dto.RedisCliStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/cli/status [get]
func (b *BaseApi) LoadRedisCliStatus(c *gin.Context) {
data, err := redisService.LoadCliStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
} }
// @Tags Database Redis // @Tags Database Redis
+32 -22
View File
@@ -2,14 +2,16 @@ package v2
import ( import (
"errors" "errors"
"github.com/1Panel-dev/1Panel/agent/buserr"
"net/http" "net/http"
"strings" "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/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service"
"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/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -455,6 +457,8 @@ func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
} }
func handleFirewallRuleError(c *gin.Context, err error) { func handleFirewallRuleError(c *gin.Context, err error) {
var businessErr buserr.BusinessError
isBusinessError := errors.As(err, &businessErr)
switch { switch {
case errors.Is(err, filter.ErrProtectedRule): case errors.Is(err, filter.ErrProtectedRule):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
@@ -462,7 +466,7 @@ func handleFirewallRuleError(c *gin.Context, err error) {
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): case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrManagedScopeChange): case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err) 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):
@@ -470,6 +474,9 @@ func handleFirewallRuleError(c *gin.Context, err error) {
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, filter.ErrRuleConflict),
errors.Is(err, repo.ErrFirewallPersistenceInvalid): 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 == "ErrInvalidParams":
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
c.Abort()
case errors.Is(err, filter.ErrVerificationFailed): case errors.Is(err, filter.ErrVerificationFailed):
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err) helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
default: default:
@@ -573,14 +580,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
return return
} }
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil { if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) { var businessErr buserr.BusinessError
helper.ErrorWithBusinessCode( if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
c, c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
http.StatusConflict, c.Abort()
"FW_BACKEND_CLEANUP_REQUIRED",
"ErrInvalidParams",
err,
)
return return
} }
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -709,19 +712,26 @@ func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
} }
func handleDockerPortGuardError(c *gin.Context, err error) { func handleDockerPortGuardError(c *gin.Context, err error) {
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) { var businessErr buserr.BusinessError
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err) if errors.As(err, &businessErr) {
return code, errorCode := http.StatusInternalServerError, ""
switch businessErr.Msg {
case "ErrDockerIptablesChainUnavailable":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
case "ErrDockerNftablesChainUnavailable":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
case "ErrInvalidParams":
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
case "ErrDockerFailed":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
}
if errorCode != "" {
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
c.Abort()
return
}
} }
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) { if errors.Is(err, docker.ErrUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerGuardInvalid) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
return
}
if errors.Is(err, service.ErrDockerUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err) helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
return return
} }
+1 -1
View File
@@ -169,7 +169,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 {
+10
View File
@@ -243,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)), UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)), AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
} }
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
millis, err := strconv.ParseInt(value, 10, 64)
if err != nil || millis <= 0 {
return terminal.Identity{}, false
}
identity.AuthLeaseUntil = time.UnixMilli(millis)
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
identity.AuthLeaseUntil = maximum
}
}
return identity, identity.Valid() return identity, identity.Valid()
} }
+10
View File
@@ -0,0 +1,10 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
}
+2
View File
@@ -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"`
@@ -0,0 +1,17 @@
package dto
type NetworkCleanupReport struct {
Deleted []NetworkCleanupItem `json:"deleted"`
Skipped []NetworkCleanupItem `json:"skipped"`
Failed []NetworkCleanupItem `json:"failed"`
}
type NetworkCleanupItem struct {
ID string `json:"id"`
Name string `json:"name"`
Reason string `json:"reason,omitempty"`
}
type NetworkCleanupTask struct {
TaskID string `json:"taskID"`
}
+10 -7
View File
@@ -51,9 +51,10 @@ type CronjobOperate struct {
Secret string `json:"secret"` Secret string `json:"secret"`
Args string `json:"args"` Args string `json:"args"`
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
} }
@@ -126,7 +127,8 @@ type CronjobInfo struct {
Secret string `json:"secret"` Secret string `json:"secret"`
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 {
@@ -169,9 +171,10 @@ type CronjobTrans struct {
SourceAccounts []string `json:"sourceAccounts"` SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"` DownloadAccount string `json:"downloadAccount"`
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"`
+11
View File
@@ -22,6 +22,17 @@ type DBBaseInfo struct {
Port int64 `json:"port"` Port int64 `json:"port"`
} }
type RedisCliInstall struct {
TaskID string `json:"taskID" validate:"omitempty,uuid"`
}
type RedisCliStatus struct {
Installed bool `json:"installed"`
TaskID string `json:"taskID"`
Status string `json:"status"`
ErrorMsg string `json:"errorMsg"`
}
// mysql // mysql
type MysqlDBSearch struct { type MysqlDBSearch struct {
PageInfo PageInfo
+10 -6
View File
@@ -49,10 +49,12 @@ type FirewallBackendOption struct {
} }
type FirewallBackendFamilyStatus struct { type FirewallBackendFamilyStatus struct {
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"`
} }
type FirewallBackendGroup struct { type FirewallBackendGroup struct {
@@ -240,8 +242,10 @@ type DockerPortGuardOperation struct {
} }
type FirewallRuleAdopt struct { type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"` Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"` 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 {
+44 -11
View File
@@ -44,6 +44,8 @@ type MonitorGPUOptions struct {
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 +57,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"`
} }
+27 -198
View File
@@ -1,222 +1,51 @@
package model package model
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
const FirewallRuleSequenceStep int64 = 1 << 32
type DockerPortGuardPolicy struct { type DockerPortGuardPolicy struct {
BaseModel BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"` UUID string `gorm:"uniqueIndex" json:"uuid"`
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"` ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"` Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"` HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"` HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"` Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"` Mode string `json:"mode"`
Sources string `gorm:"type:text" json:"-"` Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"` Description string `gorm:"type:text" json:"description"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"` NativeAction string `gorm:"default:''" json:"-"`
NativeRules string `gorm:"type:text" json:"-"` NativeRules string `gorm:"type:text" json:"-"`
Sequence int64 `gorm:"not null;default:0" json:"-"` Sequence int64 `gorm:"default:0" json:"-"`
} }
type ForwardingRule struct { type ForwardingRule struct {
BaseModel BaseModel
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"` Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"` Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"` Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"` TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"` TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"` Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
} }
type FirewallRule struct { type FirewallRule struct {
UUID string `gorm:"size:64;primaryKey" json:"uuid"` UUID string `gorm:"primaryKey" json:"uuid"`
Family string `gorm:"size:16;not null" json:"family"` Family string `json:"family"`
Protocol string `gorm:"size:32;not null" json:"protocol"` Protocol string `json:"protocol"`
SourceAddress string `gorm:"size:255" json:"sourceAddress"` SourceAddress string `json:"sourceAddress"`
SourcePort string `gorm:"size:64" json:"sourcePort"` SourcePort string `json:"sourcePort"`
DestinationAddress string `gorm:"size:255" json:"destinationAddress"` DestinationAddress string `json:"destinationAddress"`
DestinationPort string `gorm:"size:64" json:"destinationPort"` DestinationPort string `json:"destinationPort"`
Interface string `gorm:"size:128" json:"interface"` Interface string `json:"interface"`
ConnectionStates string `gorm:"type:text" json:"connectionStates"` ConnectionStates string `gorm:"type:text" json:"connectionStates"`
Action string `gorm:"size:32;not null" json:"action"` Action string `json:"action"`
Description string `gorm:"type:text" json:"description"` Description string `gorm:"type:text" json:"description"`
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"` CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
Priority *int `json:"priority,omitempty"` Priority *int `json:"priority,omitempty"`
Sequence *int64 `gorm:"index" json:"sequence,omitempty"` Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
Origin string `gorm:"size:32;not null" json:"origin"` Origin string `json:"origin"`
Owner string `gorm:"size:320;not null" json:"owner"` Owner string `json:"owner"`
Revision uint `gorm:"not null;default:1" json:"revision"` Revision uint `gorm:"default:1" json:"revision"`
}
func FirewallRuleOwner(sourceKind, sourceID string) string {
sourceKind = strings.TrimSpace(sourceKind)
sourceID = strings.TrimSpace(sourceID)
if sourceID == "" {
return sourceKind
}
return sourceKind + ":" + sourceID
}
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
return FirewallRule{}, err
}
switch normalized.NativeKind {
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
default:
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
}
record := FirewallRule{
Family: string(normalized.Scope.Family),
Protocol: normalized.Protocol,
SourceAddress: normalized.SourceAddress,
SourcePort: normalized.SourcePort,
DestinationAddress: normalized.DestinationAddress,
DestinationPort: normalized.DestinationPort,
Interface: normalized.Interface,
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
Action: string(normalized.Action),
Description: normalized.Description,
}
if normalized.Scope.Provider == filter.ProviderFirewalld {
record.Priority = normalized.Priority
}
return record, nil
}
func (rule FirewallRule) PolicyKey() string {
payload, _ := json.Marshal(struct {
Family string `json:"family"`
Protocol string `json:"protocol"`
SourceAddress string `json:"sourceAddress,omitempty"`
SourcePort string `json:"sourcePort,omitempty"`
DestinationAddress string `json:"destinationAddress,omitempty"`
DestinationPort string `json:"destinationPort,omitempty"`
Interface string `json:"interface,omitempty"`
ConnectionStates string `json:"connectionStates,omitempty"`
Action string `json:"action"`
}{
Family: rule.Family, Protocol: rule.Protocol,
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
})
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:])
}
func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
if rule.CompatibilityError != "" {
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
}
connectionStates := make([]string, 0)
if rule.ConnectionStates != "" {
connectionStates = strings.Split(rule.ConnectionStates, ",")
}
base := filter.FirewallRule{
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: connectionStates,
Action: filter.Action(rule.Action), Description: rule.Description,
}
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = rule.Priority
}
families := []filter.Family{filter.Family(rule.Family)}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
switch {
case hasIPv4 && hasIPv6:
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
families = []filter.Family{filter.FamilyIPv6}
case hasIPv4:
families = []filter.Family{filter.FamilyIPv4}
default:
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
compiled := base
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
case filter.ProviderFirewalld:
compiled.Scope.Zone = filter.FirewalldInputZone
case filter.ProviderUFW:
compiled.Scope.Chain = filter.UFWInputChain
default:
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
}
expanded, err := filter.ExpandAtomicRules(compiled)
if err != nil {
return nil, err
}
result = append(result, expanded...)
}
return result, nil
}
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
sort.SliceStable(rules, func(i, j int) bool {
left, right := rules[i], rules[j]
if provider == filter.ProviderFirewalld {
switch {
case left.Priority == nil && right.Priority != nil:
return false
case left.Priority != nil && right.Priority == nil:
return true
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
return *left.Priority < *right.Priority
}
} else {
switch {
case left.Sequence == nil && right.Sequence != nil:
return false
case left.Sequence != nil && right.Sequence == nil:
return true
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
return *left.Sequence < *right.Sequence
}
}
return left.UUID < right.UUID
})
}
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
hasIPv4, hasIPv6 := false, false
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
address = strings.TrimSpace(address)
if address == "" {
continue
}
if strings.Contains(address, ":") {
hasIPv6 = true
} else {
hasIPv4 = true
}
}
return hasIPv4, hasIPv6
} }
+40 -9
View File
@@ -33,14 +33,45 @@ 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"`
} }
-27
View File
@@ -11,10 +11,8 @@ import (
type IDockerPortGuardRepo interface { type IDockerPortGuardRepo interface {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error) ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
} }
type DockerPortGuardRepo struct{} type DockerPortGuardRepo struct{}
@@ -30,15 +28,6 @@ func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPo
return policies, err return policies, err
} }
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", true).
Order("family, sequence, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error { func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
return global.DB.WithContext(ctx). return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids). Where("read_only = ? AND uuid IN ?", false, uuids).
@@ -59,19 +48,3 @@ func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.
return nil return nil
}) })
} }
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("read_only = ?", true).
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
return err
}
if len(policies) == 0 {
return nil
}
for i := range policies {
policies[i].ReadOnly = true
}
return tx.Create(&policies).Error
})
}
+52 -9
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
var ( var (
@@ -23,6 +24,8 @@ type IFirewallRuleRepo interface {
List(context.Context, ...DBOption) ([]model.FirewallRule, error) List(context.Context, ...DBOption) ([]model.FirewallRule, error)
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
DeleteWithRevision(context.Context, string, uint) error DeleteWithRevision(context.Context, string, uint) error
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
SaveResetOrder(context.Context, []model.FirewallRule) error
} }
type FirewallRuleRepo struct { type FirewallRuleRepo struct {
@@ -87,6 +90,51 @@ func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID stri
return nil 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 { func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
return firewallDB(ctx, r.db) return firewallDB(ctx, r.db)
} }
@@ -127,18 +175,13 @@ func prepareFirewallRule(rule *model.FirewallRule) error {
} }
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} { func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
result := cloneUpdates(updates) result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
delete(result, "id") delete(result, "id")
delete(result, "uuid") delete(result, "uuid")
delete(result, "revision") delete(result, "revision")
delete(result, "created_at") delete(result, "created_at")
return result return result
} }
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
return result
}
+14 -12
View File
@@ -5,12 +5,12 @@ 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 IForwardingRuleRepo interface { type IForwardingRuleRepo interface {
List(context.Context) ([]model.ForwardingRule, error) List(context.Context) ([]model.ForwardingRule, error)
ReplaceAll(context.Context, []model.ForwardingRule) error CreateBatch(context.Context, []model.ForwardingRule) error
DeleteBatch(context.Context, []uint) error
} }
type ForwardingRuleRepo struct{} type ForwardingRuleRepo struct{}
@@ -23,14 +23,16 @@ func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule,
return rules, err return rules, err
} }
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error { func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if len(rules) == 0 {
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil { return nil
return err }
} return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
if len(rules) == 0 { }
return nil
} func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
return tx.Create(&rules).Error if len(ids) == 0 {
}) return nil
}
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
} }
+79
View File
@@ -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,9 +12,20 @@ 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 {
GetBase(opts ...DBOption) ([]model.MonitorBase, error) GetBase(opts ...DBOption) ([]model.MonitorBase, error)
GetGPU(opts ...DBOption) ([]model.MonitorGPU, 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)
@@ -26,6 +39,7 @@ type IMonitorRepo interface {
DelMonitorNet(timeForDelete time.Time) error DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
} }
func NewIMonitorRepo() IMonitorRepo { func NewIMonitorRepo() IMonitorRepo {
@@ -102,3 +116,68 @@ func (s *MonitorRepo) WithByProductName(name string) DBOption {
return g.Where("product_name = ?", name) return g.Where("product_name = ?", name)
} }
} }
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
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
}
+99 -1
View File
@@ -17,6 +17,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"
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/cmd"
@@ -109,7 +110,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 +151,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 +223,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 +250,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 +334,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
} }
@@ -382,7 +441,7 @@ func executeDiskCommand() (string, error) {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P") stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
} }
if err != nil && strings.TrimSpace(stdout) == "" { if err != nil {
return stdout, err return stdout, err
} }
var lines []string var lines []string
@@ -1021,6 +1080,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 +1110,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 +1134,22 @@ 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
}
+28 -6
View File
@@ -30,10 +30,11 @@ import (
) )
const ( const (
ResourceAlertInterval = 30 ResourceAlertInterval = 30
CheckIntervalSec = 3 CheckIntervalSec = 3
LoadCheckIntervalMin = 5 LoadCheckIntervalMin = 5
sshIPLoginWindow = 30 * time.Minute sshIPLoginWindow = 30 * time.Minute
sslAutoRenewAlertSkipDays = 31
) )
type AlertTaskHelper struct { type AlertTaskHelper struct {
@@ -529,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 {
@@ -569,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 {
@@ -864,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
daysDiff := int(math.Ceil( daysDiff := int(math.Ceil(
ssl.ExpireDate.Sub(currentDate).Hours() / 24, ssl.ExpireDate.Sub(currentDate).Hours() / 24,
)) ))
if daysDiff > 0 && int(cycle) >= daysDiff { if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain) daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate) projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
} }
@@ -872,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
return daysDiffMap, projectMap return daysDiffMap, projectMap
} }
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
}
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) { func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
currentDate := time.Now() currentDate := time.Now()
daysDiffMap := make(map[int][]string) daysDiffMap := make(map[int][]string)
+24 -57
View File
@@ -40,14 +40,13 @@ const (
appUpgradeDown appUpgradeDown
appUpgradeMutated appUpgradeMutated
appUpgradeStarted appUpgradeStarted
appUpgradeReady
appUpgradeCommitted appUpgradeCommitted
) )
const composeServiceLabel = "com.docker.compose.service"
var appUpgradeLocks sync.Map var appUpgradeLocks sync.Map
const composeServiceLabel = "com.docker.compose.service"
type appUpgradeSnapshot interface { type appUpgradeSnapshot interface {
Restore() error Restore() error
Cleanup() Cleanup()
@@ -439,15 +438,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
t.LogSuccess(logStr) t.LogSuccess(logStr)
u.phase = appUpgradeStarted u.phase = appUpgradeStarted
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady")) containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
containerNames, err := waitAppContainersReady(context.Background(), u.candidate) if discoverErr != nil {
if err != nil { t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err) } else if len(containerNames) > 0 {
return err u.candidate.ContainerName = strings.Join(containerNames, ",")
} else {
t.Log("WARNING: no containers found for the upgraded application")
} }
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
u.phase = appUpgradeReady
u.candidate.ContainerName = strings.Join(containerNames, ",")
u.candidate.Status = constant.StatusRunning u.candidate.Status = constant.StatusRunning
u.candidate.Message = "" u.candidate.Message = ""
@@ -472,6 +470,11 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil { } else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err return err
} }
if discoverErr == nil && len(containerNames) > 0 {
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
}
}
u.phase = appUpgradeCommitted u.phase = appUpgradeCommitted
u.deleteOldImages(t) u.deleteOldImages(t)
return nil return nil
@@ -591,9 +594,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
} }
func (u *appUpgradeContext) finishRollback() error { func (u *appUpgradeContext) finishRollback() error {
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
return err
}
restored := u.original restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil { if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err return err
@@ -881,28 +881,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
} }
} }
type appContainerReadinessClient interface { func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
}
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
return waitAppContainersReadyWithClient(ctx, client, install)
}
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
envContent, err := os.ReadFile(install.GetEnvPath())
if err != nil {
envContent, err = renderUpgradeEnv(&install, nil)
if err != nil {
return nil, err
}
}
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false) project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -916,36 +895,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
if len(expectedServices) == 0 { if len(expectedServices) == 0 {
return strings.Split(install.ContainerName, ","), nil return strings.Split(install.ContainerName, ","), nil
} }
options := container.ListOptions{ client, err := docker.NewDockerClient()
All: true, if err != nil {
Filters: filters.NewArgs( return nil, err
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
),
} }
containers, err := client.ContainerList(ctx, options) defer client.Close()
containers, err := client.ContainerList(context.Background(), container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
foundServices := make(map[string]bool, len(expectedServices))
containerNames := make([]string, 0, len(containers)) containerNames := make([]string, 0, len(containers))
for _, item := range containers { for _, item := range containers {
serviceName := item.Labels[composeServiceLabel] if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
if _, ok := expectedServices[serviceName]; !ok {
continue
}
if err = waitContainerReady(ctx, client, item.ID); err != nil {
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
}
foundServices[serviceName] = true
if len(item.Names) > 0 {
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/")) containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
} }
} }
for serviceName := range expectedServices {
if !foundServices[serviceName] {
return nil, fmt.Errorf("container for service %s was not created", serviceName)
}
}
sort.Strings(containerNames) sort.Strings(containerNames)
return containerNames, nil return containerNames, nil
} }
+23 -10
View File
@@ -352,22 +352,34 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
if dir != nil { if dir != nil {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App") logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
t.Log(logStr) t.Log(logStr)
cleanupFailed := false
if deleteReq.UseLifecycleScripts { if deleteReq.UseLifecycleScripts {
if err = runScript(t, &install, "uninstall"); err != nil { if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
return err cleanupFailed = true
if !deleteReq.ForceDelete {
return scriptErr
}
} }
} else { } else {
out, err := compose.Down(install.GetComposePath()) out, downErr := compose.Down(install.GetComposePath())
if err != nil && !deleteReq.ForceDelete { if downErr != nil {
return handleErr(install, err, out) cleanupFailed = true
if !deleteReq.ForceDelete {
return handleErr(install, downErr, out)
}
} }
if err = runScript(t, &install, "uninstall"); err != nil { if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
_, _ = compose.Up(install.GetComposePath()) cleanupFailed = true
return err if !deleteReq.ForceDelete {
_, _ = compose.Up(install.GetComposePath())
return scriptErr
}
} }
} }
t.LogSuccess(logStr) if !cleanupFailed {
t.LogSuccess(logStr)
}
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
@@ -466,8 +478,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
} }
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil) uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
go func() { go func() {
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete { if err := uninstallTask.Execute(); err != nil {
install.Status = constant.StatusError install.Status = constant.StatusError
install.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), &install) _ = appInstallRepo.Save(context.Background(), &install)
} }
}() }()
+95 -35
View File
@@ -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
@@ -92,6 +92,7 @@ type IContainerService interface {
Inspect(req dto.InspectReq) (string, error) Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error DeleteNetwork(req dto.BatchDelete) error
CleanNetworks() (*dto.NetworkCleanupTask, error)
CreateNetwork(req dto.NetworkCreate) error CreateNetwork(req dto.NetworkCreate) error
DeleteVolume(req dto.BatchDelete) error DeleteVolume(req dto.BatchDelete) error
CreateVolume(req dto.VolumeCreate) error CreateVolume(req dto.VolumeCreate) error
@@ -112,7 +113,9 @@ 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) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
@@ -123,7 +126,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 +209,32 @@ 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() c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
images, _ := client.ImageList(c, image.ListOptions{All: true}) if !containersOnly {
data.ImageCount = len(images) images, _ := client.ImageList(c, image.ListOptions{All: true})
repo, _ := imageRepoRepo.List() data.ImageCount = len(images)
data.RepoCount = len(repo) repo, _ := imageRepoRepo.List()
templates, _ := composeRepo.List() data.RepoCount = len(repo)
data.ComposeTemplateCount = len(templates) templates, _ := composeRepo.List()
networks, _ := client.NetworkList(c, network.ListOptions{}) data.ComposeTemplateCount = len(templates)
data.NetworkCount = len(networks) networks, _ := client.NetworkList(c, network.ListOptions{})
volumes, _ := client.VolumeList(c, volume.ListOptions{}) data.NetworkCount = len(networks)
data.VolumeCount = len(volumes.Volumes) volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.ComposeCount = loadComposeCount(client) data.VolumeCount = len(volumes.Volumes)
containers, _ := client.ContainerList(c, container.ListOptions{All: true}) data.ComposeCount = loadComposeCount(c, client)
}
containers, err := client.ContainerList(c, 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,27 +300,67 @@ 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}) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
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
} }
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
return loadCpuAndMem(ctx, client, id)
}), nil
}
// A fixed worker pool bounds Docker stats requests, including for legacy callers
// that request all containers. Stopped containers do not need a stats sample.
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
datas := make([]dto.ContainerListStats, len(list)) datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup for i, item := range list {
wg.Add(len(list)) datas[i].ContainerID = item.ID
for i := 0; i < len(list); i++ {
go func(index int, item container.Summary) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(i, list[i])
} }
jobs := make(chan int)
var wg sync.WaitGroup
for worker := 0; worker < min(8, len(list)); worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for index := range jobs {
if ctx.Err() != nil || list[index].State != "running" {
continue
}
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
datas[index] = load(sampleCtx, list[index].ID)
cancel()
}
}()
}
dispatch:
for index := range list {
select {
case <-ctx.Done():
break dispatch
case jobs <- index:
}
}
close(jobs)
wg.Wait() wg.Wait()
return datas, nil return datas
} }
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
@@ -484,6 +532,10 @@ func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) {
} }
func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bool) error { func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bool) error {
return u.containerCreate(req, inThread, "")
}
func (u *ContainerService) containerCreate(req dto.ContainerOperate, inThread bool, taskName string) error {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return err return err
@@ -497,7 +549,10 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
return buserr.New("ErrContainerName") return buserr.New("ErrContainerName")
} }
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) if taskName == "" {
taskName = task.GetTaskName(req.Name, task.TaskCreate, task.TaskScopeContainer)
}
taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
unlock() unlock()
_ = client.Close() _ = client.Close()
@@ -558,6 +613,11 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
}, nil) }, nil)
if inThread { if inThread {
if err := taskItem.Prepare(); err != nil {
unlock()
_ = client.Close()
return err
}
go func() { go func() {
defer unlock() defer unlock()
defer client.Close() defer client.Close()
@@ -1715,11 +1775,11 @@ 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 {
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
} }
@@ -1942,11 +2002,11 @@ func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports) return docker.SimplifyPorts(ports)
} }
func loadComposeCount(client *client.Client) int { func loadComposeCount(ctx context.Context, client *client.Client) int {
options := container.ListOptions{All: true} options := container.ListOptions{All: true}
options.Filters = filters.NewArgs() options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel) options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options) list, err := client.ContainerList(ctx, options)
if err != nil { if err != nil {
return 0 return 0
} }
@@ -0,0 +1,71 @@
package service
import (
"fmt"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
)
var networkCleanupMu sync.Mutex
func (u *ContainerService) CleanNetworks() (*dto.NetworkCleanupTask, error) {
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey("Network"), task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
return nil, err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
networkCleanupMu.Lock()
defer networkCleanupMu.Unlock()
if err := t.TaskCtx.Err(); err != nil {
return err
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
return executeNetworkCleanup(t, cli)
}, nil)
go func() {
if err := taskItem.Execute(); err != nil {
global.LOG.Errorf("network cleanup task %s failed: %v", taskItem.TaskID, err)
}
}()
return &dto.NetworkCleanupTask{TaskID: taskItem.TaskID}, nil
}
func executeNetworkCleanup(t *task.Task, cli docker.NetworkCleanupClient) error {
t.Log(i18n.GetMsgByKey("PruneStart"))
report, err := docker.CleanUnusedNetworks(t.TaskCtx, cli, func(status string, item dto.NetworkCleanupItem) {
key := "NetworkCleanupDeleted"
if status == "skipped" || status == "failed" {
key = map[string]string{
"protected": "NetworkCleanupProtected",
"container_connected": "NetworkCleanupConnected",
"network_in_use": "NetworkCleanupConnected",
"unsupported_network": "NetworkCleanupUnsupported",
"already_removed": "NetworkCleanupGone",
"inspect_failed": "NetworkCleanupInspectFailed",
"remove_failed": "NetworkCleanupRemoveFailed",
}[item.Reason]
}
t.Log(i18n.GetMsgWithMap(key, map[string]interface{}{"name": item.Name, "id": item.ID}))
})
if report != nil {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": len(report.Deleted), "skipped": len(report.Skipped), "failed": len(report.Failed),
}))
}
if err != nil {
return err
}
if len(report.Failed) > 0 {
return fmt.Errorf("%s", i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
@@ -0,0 +1,113 @@
package service
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type networkTaskClient struct{ fail bool }
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
}
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
return nil, nil
}
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
n := network.Inspect{}
if id == "connected" {
n.Containers = map[string]network.EndpointResource{"container-id": {}}
}
return n, nil
}
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
if id == "race" {
return errdefs.Conflict(errors.New("has active endpoints"))
}
if id != "unused" {
return errors.New("unexpected removal")
}
if f.fail {
return errors.New("remove failed")
}
return nil
}
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
dir := t.TempDir()
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
defer sqlDB.Close()
if err := db.AutoMigrate(&model.Task{}); err != nil {
t.Fatal(err)
}
global.DB = nil
global.TaskDB = db
global.Dir.TaskDir = dir
i18n.Init()
for _, fail := range []bool{false, true} {
name := "success"
wantStatus := constant.StatusSuccess
if fail {
name = "partial failure"
wantStatus = constant.StatusFailed
}
t.Run(name, func(t *testing.T) {
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
t.Fatal(err)
}
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
err = item.Execute()
if (err != nil) != fail {
t.Fatalf("unexpected execution error: %v", err)
}
var saved model.Task
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
t.Fatal(err)
}
if saved.Status != wantStatus {
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
}
content, err := os.ReadFile(saved.LogFile)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q in log: %s", want, content)
}
}
want := "Deleted network [free]"
if fail {
want = "Failed to remove network [free]"
}
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q", want)
}
})
}
}
+92 -26
View File
@@ -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.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount item.AlertCount = alertInfo.SendCount
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
} 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")
@@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
} }
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" { if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
createAlert := dto.AlertCreate{ createAlert := dto.AlertCreate{
Title: req.AlertTitle, Title: req.AlertTitle,
SendCount: req.AlertCount, SendCount: req.AlertCount,
Method: req.AlertMethod, Method: req.AlertMethod,
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
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return err return err
} }
updateAlert := dto.AlertCreate{ updateAlert := dto.AlertCreate{
Title: req.AlertTitle, Title: req.AlertTitle,
SendCount: req.AlertCount, SendCount: req.AlertCount,
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 {
+1
View File
@@ -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 {
+35 -6
View File
@@ -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,
+11 -2
View File
@@ -464,12 +464,14 @@ func loadDiskInfo() []dto.DiskInfo {
cmd.PipeCommand{Name: "awk", Args: []string{format}}, cmd.PipeCommand{Name: "awk", Args: []string{format}},
) )
if err != nil { 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)) cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunPipe( stdout, err = cmdMgr2.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}}, cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}}, cmd.PipeCommand{Name: "awk", Args: []string{format}},
) )
if err != nil && strings.TrimSpace(stdout) == "" { if err != nil {
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
return datas return datas
} }
} }
@@ -586,6 +588,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 {
@@ -595,7 +600,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:
+58 -4
View File
@@ -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"
@@ -23,6 +29,11 @@ import (
type RedisService struct{} type RedisService struct{}
const redisCliTaskName = "RedisCliEnable"
// 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 +44,8 @@ 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) (*dto.RedisCliStatus, error)
LoadCliStatus() (*dto.RedisCliStatus, error)
} }
func NewIRedisService() IRedisService { func NewIRedisService() IRedisService {
@@ -71,20 +83,62 @@ func (u *RedisService) CheckHasCli() bool {
return false return false
} }
for _, item := range containerLists { for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" { if len(item.Names) > 0 && strings.TrimPrefix(item.Names[0], "/") == "1Panel-redis-cli-tools" {
return true return true
} }
} }
return false return false
} }
func (u *RedisService) InstallCli() error { func (u *RedisService) LoadCliStatus() (*dto.RedisCliStatus, error) {
result := &dto.RedisCliStatus{}
latest, err := taskRepo.GetFirst(repo.WithByName(redisCliTaskName), repo.WithByType(task.TaskScopeContainer), repo.WithOrderDesc("created_at"))
if errors.Is(err, gorm.ErrRecordNotFound) {
result.Installed = u.CheckHasCli()
return result, nil
}
if err != nil {
return nil, err
}
result.TaskID = latest.ID
result.Status = latest.Status
result.ErrorMsg = latest.ErrorMsg
result.Installed = u.CheckHasCli()
return result, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) {
if !redisCliInstallMutex.TryLock() {
return nil, buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
status, err := u.LoadCliStatus()
if err != nil {
return nil, err
}
if status.Status == constant.StatusExecuting || status.Installed {
return status, nil
}
if req.TaskID == "" {
req.TaskID = uuid.NewString()
}
// Never reuse an existing task ID: doing so would truncate its log.
if _, err := taskRepo.GetFirst(taskRepo.WithByID(req.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return nil, err
}
return nil, buserr.New("TaskIsExecuting")
}
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) if err := (&ContainerService{}).containerCreate(item, true, redisCliTaskName); err != nil {
return nil, err
}
return &dto.RedisCliStatus{TaskID: req.TaskID, Status: constant.StatusExecuting}, nil
} }
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
+5 -3
View File
@@ -18,7 +18,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
) )
const dockerNftablesMinVersion = "29.0.0" const dockerNftablesMinVersion = "29.0.0"
@@ -84,7 +85,7 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion) return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
} }
if backend == constant.FirewallProviderNftables { if backend == constant.FirewallProviderNftables {
if err := docker_guard.CheckIPv4Forwarding(); err != nil { if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
return err return err
} }
} }
@@ -282,7 +283,8 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
delete(daemonMap, "ipv6") delete(daemonMap, "ipv6")
delete(daemonMap, "fixed-cidr-v6") delete(daemonMap, "fixed-cidr-v6")
delete(daemonMap, "ip6tables") delete(daemonMap, "ip6tables")
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables { backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
delete(daemonMap, "experimental") delete(daemonMap, "experimental")
} }
} }
+3 -2
View File
@@ -37,8 +37,9 @@ var (
clamRepo = repo.NewIClamRepo() clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo() monitorRepo = repo.NewIMonitorRepo()
settingRepo = repo.NewISettingRepo() settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo() forwardingRuleRepo = repo.NewIForwardingRuleRepo()
backupRepo = repo.NewIBackupRepo()
websiteRepo = repo.NewIWebsiteRepo() websiteRepo = repo.NewIWebsiteRepo()
websiteDomainRepo = repo.NewIWebsiteDomainRepo() websiteDomainRepo = repo.NewIWebsiteDomainRepo()
+28 -9
View File
@@ -439,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil { if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
return err return err
} }
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1) taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil { if err != nil {
return err return err
} }
go func() { go func() {
taskItem.AddSubTask(c.Name, func(t *task.Task) error { taskItem.AddSubTask(taskName, func(t *task.Task) error {
t.LogStart(c.Name) logFileTaskSources(t, c.Files)
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
compressType := files.CompressType(c.Type) compressType := files.CompressType(c.Type)
dstFile := filepath.Join(c.Dst, c.Name) dstFile := filepath.Join(c.Dst, c.Name)
success := false success := false
@@ -516,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil { if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
return err return err
} }
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1) taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil { if err != nil {
return err return err
} }
go func() { go func() {
taskItem.AddSubTask(c.Path, func(t *task.Task) error { taskItem.AddSubTask(taskName, func(t *task.Task) error {
t.LogStart(c.Path) logFileTaskSources(t, []string{c.Path})
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
dstExisted := fo.Stat(c.Dst) dstExisted := fo.Stat(c.Dst)
parentDir := filepath.Dir(c.Dst) parentDir := filepath.Dir(c.Dst)
if !fo.Stat(parentDir) { if !fo.Stat(parentDir) {
@@ -914,6 +918,12 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options) return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
} }
func logFileTaskSources(t *task.Task, sources []string) {
for _, source := range sources {
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
}
}
func (f *FileService) MvFile(m request.FileMove) error { func (f *FileService) MvFile(m request.FileMove) error {
fo := files.NewFileOp() fo := files.NewFileOp()
if err := validateFileMove(fo, m); err != nil { if err := validateFileMove(fo, m); err != nil {
@@ -925,15 +935,24 @@ func (f *FileService) MvFile(m request.FileMove) error {
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) { if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
return buserr.New("TaskIsExecuting") return buserr.New("TaskIsExecuting")
} }
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1) nameKey := "FileTaskCopy"
if m.Type == "cut" {
nameKey = "FileTaskMove"
}
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
if err != nil { if err != nil {
fileTransferLocks.Release(m.TaskID) fileTransferLocks.Release(m.TaskID)
return err return err
} }
go func() { go func() {
defer fileTransferLocks.Release(m.TaskID) defer fileTransferLocks.Release(m.TaskID)
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error { taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
t.LogStart(m.NewPath) logFileTaskSources(t, m.OldPaths)
logFileTaskSources(t, m.CoverPaths)
if m.Name != "" {
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
}
err := f.moveFileWithContext(t.TaskCtx, m) err := f.moveFileWithContext(t.TaskCtx, m)
if err != nil && t.TaskCtx.Err() != nil { if err != nil && t.TaskCtx.Err() != nil {
return t.TaskCtx.Err() return t.TaskCtx.Err()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"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/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
-65
View File
@@ -1,65 +0,0 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func selectedDockerFirewallBackend(fallback string) string {
selected := configuredDockerFirewallBackend()
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
fallback = strings.ToLower(strings.TrimSpace(fallback))
if fallback == constant.FirewallProviderNftables {
return fallback
}
return constant.FirewallProviderIptables
}
func configuredDockerFirewallBackend() string {
if global.DB == nil {
return ""
}
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
return ""
}
func selectedSystemFirewallClient() (lifecycle.Client, error) {
if provider := configuredSystemFirewallBackend(); provider != "" {
return lifecycle.NewClientFor(provider)
}
client, err := lifecycle.NewClient()
if err != nil {
return nil, err
}
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
return client, nil
}
func configuredSystemFirewallBackend() string {
if global.DB == nil {
return ""
}
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
return strings.TrimSpace(provider)
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
return selectedSystemFirewallClient()
}
func selectedSystemFirewallProvider() (string, error) {
client, err := selectedSystemFirewallClient()
if err != nil {
return "", err
}
return client.Name(), nil
}
+155 -308
View File
@@ -5,23 +5,20 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"reflect"
"slices" "slices"
"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/model"
"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"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "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/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"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"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"
) )
@@ -37,35 +34,61 @@ type FirewallSettingService struct{}
var firewallWhitelistMu sync.Mutex var firewallWhitelistMu sync.Mutex
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
func firewallBackendCleanupRequired(current, target string) error {
return fmt.Errorf(
"%w: current backend %s still contains 1Panel runtime rules; clean it up before switching to %s",
ErrFirewallBackendCleanupRequired,
current,
target,
)
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error { func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) { firewallWhitelistMu.Lock()
return append(current, request.Rule), nil defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
return err
}
current = append(current, request.Rule)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
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) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) { firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
return err
}
index, err := findPortWhitelistRule(current, request.OldRule) index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil { if err != nil {
return nil, err return err
} }
current[index] = request.Rule current[index] = request.Rule
return current, nil current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
if err != nil {
return err
}
return nil
}) })
} }
@@ -73,156 +96,33 @@ func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, reques
if request.Rule == nil { if request.Rule == nil {
return fmt.Errorf("select one firewall port whitelist rule to delete") return fmt.Errorf("select one firewall port whitelist rule to delete")
} }
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return nil, err
}
return slices.Delete(current, index, index+1), nil
})
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil {
return false
}
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
firewallWhitelistMu.Lock() firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock() defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer firewallRuleMutationMu.Unlock()
defer filterruntime.InvalidateInventory()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx) current, err := loadPortWhitelistSetting(tx)
if err != nil { if err != nil {
return err return err
} }
desired, err := change(current) index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil { if err != nil {
return err return err
} }
desired, err = firewall.ValidatePortWhitelist(desired) current = slices.Delete(current, index, index+1)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil { if err != nil {
return err return err
} }
value, err := json.Marshal(desired) value, err := json.Marshal(current)
if err != nil { if err != nil {
return err return err
} }
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}). err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error if err != nil {
}) return err
}
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
rules, err := record.RulesForProvider(provider)
if err != nil {
return err
}
for _, rule := range rules {
if filter.RuleMatchesPortWhitelist(rule, ports) {
return filter.ErrProtectedRule
} }
} return nil
return nil
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err
}
var rules []firewall.PortWhitelist
err := json.Unmarshal([]byte(setting.Value), &rules)
return rules, err
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil {
return "", err
}
return loadSSHPortValues(directives)[0], nil
}
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
result := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
result = append(result, entry)
}
}
return result
}
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
var sshPort string
for i := range entries {
entry := &entries[i]
if entry.Type == "" || entry.Port != "" {
continue
}
switch entry.Type {
case firewall.PortWhitelistTypePanel:
entry.Port = LoadPanelPort()
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
}
}
return firewall.ValidatePortWhitelist(entries)
}
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
for i := range entries {
if entries[i].Type == serviceType {
if len(ports) == 0 {
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
}
entries[i].Port = ports[0]
}
}
return entries, nil
}) })
} }
@@ -233,9 +133,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
for _, name := range lifecycle.InstalledProviders() { for _, name := range lifecycle.InstalledProviders() {
installed[name] = true installed[name] = true
} }
result.System.Selected = configuredSystemFirewallBackend() systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
result.System.Selected = strings.TrimSpace(systemBackend)
if result.System.Selected == "" { if result.System.Selected == "" {
if client, err := lifecycle.NewClient(); err == nil { if client, err := lifecycle.NewClient(""); err == nil {
result.System.Selected = client.Name() result.System.Selected = client.Name()
} }
} }
@@ -248,16 +149,16 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
} { } {
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.System.Selected { if option.Installed && name == result.System.Selected {
client, err := lifecycle.NewClientFor(name) client, err := lifecycle.NewClient(name)
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else if supportsManagedFilterChains(name) { } else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base") overview, err := loadSystemFirewallOverview(name, "base")
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} }
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4) option.Initialized, option.Bound = overview.IsInit, overview.IsBind
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6) option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
} else if option.Active, err = client.Status(); err != nil { } else if option.Active, err = client.Status(); err != nil {
option.Message = err.Error() option.Message = err.Error()
} }
@@ -270,32 +171,29 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
result.System.Options = append(result.System.Options, option) result.System.Options = append(result.System.Options, option)
} }
result.Forwarding.Selected = configuredForwardingBackend() forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
if result.Forwarding.Selected == "" {
result.Forwarding.Selected = constant.FirewallProviderIptables
}
result.Forwarding.Current = result.Forwarding.Selected result.Forwarding.Current = result.Forwarding.Selected
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 := newForwardingManagerFor(name) manager, err := newForwardingAdapterFor(name)
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else if status, err := manager.Status(); err != nil {
option.Message = err.Error()
} else { } else {
option.Initialized, option.Bound = status.IsInit, status.IsBind status, statusErr := loadForwardingFirewallOverview(manager)
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4) option.IPv4, option.IPv6 = status.IPv4, status.IPv6
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6) if statusErr != nil {
option.IPv4 = dto.FirewallBackendFamilyStatus{ option.Message = statusErr.Error()
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound, } else {
option.Initialized, option.Bound = status.IsInit, status.IsBind
} }
option.IPv6 = dto.FirewallBackendFamilyStatus{ if name == constant.FirewallProviderIptables && !option.IPv6.Available {
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound, if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
} option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
if name == constant.FirewallProviderIptables {
if commands, commandErr := lifecycle.ResolveIptablesCommands(); commandErr == nil {
option.IPv6.Available = option.IPv6.Available && commands.IPv6Available()
if !commands.IPv6Available() {
option.IPv6.Reason = docker_guard.ReasonCommandMissing
}
} }
} }
} }
@@ -313,7 +211,11 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
if dockerInstalled { if dockerInstalled {
dockerVersion = loadDockerEngineVersion(ctx) dockerVersion = loadDockerEngineVersion(ctx)
} }
result.Docker.Selected = configuredDockerFirewallBackend() dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
result.Docker.Selected = dockerBackend
}
result.Docker.Current = result.Docker.Selected result.Docker.Current = result.Docker.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{ option := dto.FirewallBackendOption{
@@ -326,14 +228,14 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
option.Active = false option.Active = false
} }
if option.Active { if option.Active {
guard := docker_guard.NewRuntime(name) guard := newDockerFirewallRuntime(name)
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6) ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
option.Initialized = ipv4.Initialized || ipv6.Initialized option.Initialized = ipv4.Initialized || ipv6.Initialized
option.Bound = ipv4.Bound || ipv6.Bound option.Bound = ipv4.Bound || ipv6.Bound
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
} }
result.Docker.Options = append(result.Docker.Options, option) result.Docker.Options = append(result.Docker.Options, option)
@@ -353,32 +255,6 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
return result, err return result, err
} }
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
switch provider {
case constant.FirewallProviderIptables:
return iptables_helper.LoadFamilyInitStatus(family, "base")
case constant.FirewallProviderNftables:
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
default:
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
}
}
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus {
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() {
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
}
}
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
return dto.FirewallBackendFamilyStatus{
Available: err == nil,
Initialized: initialized,
Bound: bound,
}
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil { if err := lockFirewallLifecycleIdle(); err != nil {
return err return err
@@ -387,7 +263,7 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables { if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem) return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
} }
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" { if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend) return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
} }
switch request.Subsystem { switch request.Subsystem {
@@ -411,64 +287,14 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
} }
} }
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error { func NewIFirewallSettingService() IFirewallSettingService {
guard := docker_guard.NewRuntime(request.Backend) return &FirewallSettingService{}
if request.Operation == "cleanup" {
if err := guard.Cleanup(); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
current = alternateDirectBackend(request.Backend)
}
initialized, err := dockerGuardBackendInitialized(current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
return nil
}
func dockerGuardBackendInitialized(backend string) (bool, error) {
guard := docker_guard.NewRuntime(backend)
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
} }
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer firewallRuleMutationMu.Unlock()
if _, err := lifecycle.NewClientFor(request.Backend); err != nil { if _, err := lifecycle.NewClient(request.Backend); err != nil {
return err return err
} }
if request.Operation == "cleanup" { if request.Operation == "cleanup" {
@@ -476,7 +302,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
} }
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 {
previous = client.Name() previous = client.Name()
} }
} }
@@ -486,7 +312,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
return err return err
} }
if initialized { if initialized {
return firewallBackendCleanupRequired(previous, request.Backend) return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
} }
} }
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil { if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
@@ -512,21 +338,14 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
} }
func systemFirewallBackendInitialized(backend string) (bool, error) { func systemFirewallBackendInitialized(backend string) (bool, error) {
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor) client, err := lifecycle.NewClient(backend)
}
func systemFirewallBackendInitializedWithClientFactory(
backend string,
newClient func(string) (lifecycle.Client, error),
) (bool, error) {
client, err := newClient(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
} }
return false, err return false, err
} }
if supportsManagedFilterChains(backend) { if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family) initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
@@ -544,30 +363,8 @@ func systemFirewallBackendInitializedWithClientFactory(
return client.Status() return client.Status()
} }
func cleanupSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
return newIptablesHelperManager().Cleanup()
case constant.FirewallProviderNftables:
return newNftablesHelperManager().Cleanup()
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
}
func cleanupInactiveSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
return (&iptables_helper.Manager{}).Cleanup()
case constant.FirewallProviderNftables:
return (&nftables_helper.Manager{}).Cleanup()
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
}
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
manager, err := newForwardingManagerFor(request.Backend) manager, err := newForwardingAdapterFor(request.Backend)
if err != nil { if err != nil {
return err return err
} }
@@ -585,7 +382,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
if request.Operation == "select" { if request.Operation == "select" {
current := previous current := previous
if current == "" { if current == "" {
detected, err := newForwardingManager() detected, err := newForwardingAdapter()
if err != nil { if err != nil {
return err return err
} }
@@ -596,7 +393,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
return err return err
} }
if current != request.Backend && initialized { if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend) return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
} }
} }
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil { if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
@@ -610,7 +407,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
} }
func forwardingBackendInitialized(backend string) (bool, error) { func forwardingBackendInitialized(backend string) (bool, error) {
manager, err := newForwardingManagerFor(backend) manager, err := newForwardingAdapterFor(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
@@ -629,9 +426,59 @@ func forwardingBackendInitialized(backend string) (bool, error) {
return false, nil return false, nil
} }
func alternateDirectBackend(backend string) string { func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
if backend == constant.FirewallProviderNftables { guard := newDockerFirewallRuntime(request.Backend)
return constant.FirewallProviderIptables if request.Operation == "cleanup" {
if err := guard.Cleanup(); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
} }
return constant.FirewallProviderNftables previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
current = constant.FirewallProviderNftables
if request.Backend == constant.FirewallProviderNftables {
current = constant.FirewallProviderIptables
}
}
initialized, err := dockerGuardBackendInitialized(current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
return nil
}
func dockerGuardBackendInitialized(backend string) (bool, error) {
guard := newDockerFirewallRuntime(backend)
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+302 -313
View File
@@ -4,9 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"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/model"
@@ -16,11 +18,19 @@ 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"
"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/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() (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
@@ -31,7 +41,7 @@ type IForwardingService interface {
} }
type ForwardingService struct { type ForwardingService struct {
managerFactory func() (*forwarding.Manager, error) clientFactory func() (forwarding.Adapter, error)
rules repo.IForwardingRuleRepo rules repo.IForwardingRuleRepo
enabled func() (bool, error) enabled func() (bool, error)
persistBackend func(string) error persistBackend func(string) error
@@ -39,43 +49,27 @@ type ForwardingService struct {
} }
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
var forwardingMutationMu sync.Mutex
const ( var forwardingMutationMu sync.Mutex
forwardingSyncConverged = "converged"
forwardingSyncMissing = "missing"
forwardingSyncRuntimeOnly = "runtime_only"
)
var ( var (
forwardingSyncStateMu sync.RWMutex forwardingSyncStateMu sync.RWMutex
forwardingLastSyncErr error forwardingLastSyncErr error
) )
func NewIForwardingService() IForwardingService {
return newForwardingService()
}
func newForwardingService() *ForwardingService {
return &ForwardingService{
managerFactory: newForwardingManager,
rules: repo.NewIForwardingRuleRepo(),
enabled: forwardingPersistedEnabled,
markEnabled: func() error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
},
persistBackend: func(backend string) error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
},
}
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) { func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected := configuredForwardingBackend() selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
baseInfo := dto.FirewallSubsystemStatus{ selected = strings.TrimSpace(selected)
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(), if selected == "" {
selected = constant.FirewallProviderIptables
} }
manager, err := s.managerFactory() baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(),
}
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
baseInfo.Name += "-forward"
}
manager, err := s.clientFactory()
if err != nil { if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) { if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled baseInfo.Reason = constant.FirewallBackendNotInstalled
@@ -83,39 +77,41 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
} }
return baseInfo, err return baseInfo, err
} }
status, err := manager.Status() client, err := lifecycle.NewClient(manager.Name())
if err != nil { if err != nil {
return baseInfo, err return baseInfo, err
} }
version, versionErr := client.Version()
status, statusErr := loadForwardingFirewallOverview(manager)
if err := errors.Join(versionErr, statusErr); err != nil {
return baseInfo, err
}
baseInfo.IsExist = true baseInfo.IsExist = true
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name baseInfo.Name, baseInfo.Backend = manager.Name(), manager.Name()
baseInfo.Version = status.Version if baseInfo.Backend == constant.FirewallProviderIptables || baseInfo.Backend == constant.FirewallProviderNftables {
baseInfo.Name += "-forward"
}
baseInfo.Version = version
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 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4) baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6) 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 loadForwardingFamilyInfo(manager *forwarding.Manager, backend, family string) dto.FirewallBackendFamilyStatus {
initialized, bound, err := manager.FamilyStatus(family)
available := err == nil
if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
commands, commandErr := lifecycle.ResolveIptablesCommands()
available = available && commandErr == nil && commands.IPv6Available()
}
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}
}
func forwardingDisplayName(backend string) string {
switch backend {
case constant.FirewallProviderIptables, constant.FirewallProviderNftables:
return backend + "-forward"
default:
return backend
}
}
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
if request.Strategy != "" { if request.Strategy != "" {
return 0, nil, nil return 0, nil, nil
@@ -124,11 +120,11 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
manager, err := s.managerFactory() manager, err := s.clientFactory()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
runtime, err := manager.List("", "") runtime, err := manager.List()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@@ -178,24 +174,18 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
return int64(total), items, nil return int64(total), items, nil
} }
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool { func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
values := []string{ count := 0
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP, for _, rule := range request.Rules {
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(), if rule.Operation == "add" {
} count += strings.Count(rule.Protocol, "/") + 1
for _, value := range values { }
if strings.Contains(strings.ToLower(value), keyword) { if count > filter.MaxAtomicExpansion {
return true return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
} }
} }
return false
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Rules))
operation := task.TaskCreate operation := task.TaskCreate
for i, rule := range request.Rules { for _, rule := range request.Rules {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
if rule.Operation != "add" { if rule.Operation != "add" {
operation = task.TaskUpdate operation = task.TaskUpdate
} }
@@ -203,48 +193,26 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
if forwardingOperationsOnlyRemove(request.Rules) { if forwardingOperationsOnlyRemove(request.Rules) {
operation = task.TaskDelete operation = task.TaskDelete
} }
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error { taskItem, err := task.NewTask(firewallTaskName(operation, firewallTaskForwarding, ""), operation, task.TaskScopeFirewall, "", 0)
return s.operateRules(ctx, request)
})
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil { if err != nil {
return err return dto.FilterChainOperationResponse{}, err
} }
desired, err := applyForwardingOperations(forwardingRulesFromModels(stored), request.Rules) taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
if errors.Is(err, forwarding.ErrRuleExists) { return s.operateRules(t.TaskCtx, request, t)
return buserr.New("ErrRecordExist") }, nil, 0, 0)
} else if err != nil { if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
return err taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, err
} }
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); err != nil { go func() { _ = taskItem.Execute() }()
return err return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
if err := s.reconcile(desired); err != nil {
recordForwardingSyncError(err)
if request.ForceDelete && forwardingOperationsOnlyRemove(request.Rules) {
if global.LOG != nil {
global.LOG.Error(err)
}
return nil
}
return err
}
recordForwardingSyncError(nil)
return nil
} }
func (s *ForwardingService) Enable() error { func (s *ForwardingService) Enable() error {
forwardingMutationMu.Lock() forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock() defer forwardingMutationMu.Unlock()
manager, err := s.managerFactory() manager, err := s.clientFactory()
if err != nil { if err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
@@ -253,7 +221,7 @@ func (s *ForwardingService) Enable() error {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
if err := s.activateManager(manager); err != nil { if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
@@ -262,14 +230,12 @@ func (s *ForwardingService) Enable() error {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
err = manager.Reconcile(forwardingRulesFromModels(rules)) err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
func (s *ForwardingService) QueueInitialization( func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
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
} }
@@ -277,13 +243,13 @@ func (s *ForwardingService) QueueInitialization(
if err != nil { if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err) return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
} }
var manager *forwarding.Manager var manager forwarding.Adapter
var backend string var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
forwardingMutationMu.Lock() forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock() defer forwardingMutationMu.Unlock()
var err error var err error
manager, err = s.managerFactory() manager, err = s.clientFactory()
if err != nil { if err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
@@ -294,7 +260,7 @@ func (s *ForwardingService) QueueInitialization(
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
if err := s.activateManager(manager); err != nil { if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
@@ -308,7 +274,7 @@ func (s *ForwardingService) QueueInitialization(
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
err = manager.Reconcile(forwardingRulesFromModels(rules)) err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
}, nil) }, nil)
@@ -319,131 +285,43 @@ func (s *ForwardingService) QueueInitialization(
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
} }
func (s *ForwardingService) Restore(ctx context.Context) error { func NewIForwardingService() IForwardingService {
forwardingMutationMu.Lock() return newForwardingService()
defer forwardingMutationMu.Unlock() }
enabled, err := s.forwardingEnabled()
if err != nil || !enabled { func loadForwardPolicy(command string) (string, error) {
if err != nil { if !cmd.Which(command) {
recordForwardingSyncError(err) command += "-nft"
if !cmd.Which(command) {
return "", nil
} }
return err
} }
manager, err := s.managerFactory() output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
if err != nil { if err != nil {
recordForwardingSyncError(err) return "", err
return err
} }
stored, err := s.rules.List(ctx) for _, line := range strings.Split(output, "\n") {
if err != nil { fields := strings.Fields(line)
recordForwardingSyncError(err) if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
return err if fields[2] != "ACCEPT" && fields[2] != "DROP" {
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
}
return fields[2], nil
}
} }
if err := s.activateManager(manager); err != nil { return "", errors.New("FORWARD default policy was not found")
recordForwardingSyncError(err)
return err
}
err = manager.Reconcile(forwardingRulesFromModels(stored))
recordForwardingSyncError(err)
return err
} }
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error { func lastForwardingSyncError() string {
manager, err := s.managerFactory() forwardingSyncStateMu.RLock()
if err != nil { defer forwardingSyncStateMu.RUnlock()
return err if forwardingLastSyncErr == nil {
return ""
} }
return s.reconcileWithManager(manager, rules) return forwardingLastSyncErr.Error()
} }
func (s *ForwardingService) reconcileWithManager(manager *forwarding.Manager, rules []forwarding.Rule) error { func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
enabled, err := s.forwardingEnabled()
if err != nil || !enabled {
return err
}
if err := s.activateManager(manager); err != nil {
return err
}
return manager.Reconcile(rules)
}
func (s *ForwardingService) activateManager(manager *forwarding.Manager) error {
if err := s.saveForwardingBackend(manager.Name()); err != nil {
return err
}
return manager.Enable()
}
func (s *ForwardingService) forwardingEnabled() (bool, error) {
if s.enabled != nil {
return s.enabled()
}
return forwardingPersistedEnabled()
}
func (s *ForwardingService) saveForwardingBackend(backend string) error {
if s.persistBackend != nil {
return s.persistBackend(backend)
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
}
func (s *ForwardingService) persistForwardingEnabled() error {
if s.markEnabled != nil {
return s.markEnabled()
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
}
func forwardingPersistedEnabled() (bool, error) {
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
return status == constant.StatusEnable, err
}
func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule {
rules := make([]forwarding.Rule, 0, len(stored))
for _, rule := range stored {
rules = append(rules, forwarding.Rule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
})
}
return rules
}
func forwardingRuleModels(rules []forwarding.Rule) []model.ForwardingRule {
stored := make([]model.ForwardingRule, 0, len(rules))
for _, rule := range rules {
stored = append(stored, model.ForwardingRule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
})
}
return stored
}
type forwardingInventoryItem struct {
ID uint
Rule forwarding.Rule
IsDesired bool
IsRuntime bool
}
func (i forwardingInventoryItem) SyncStatus() string {
switch {
case i.IsDesired && i.IsRuntime:
return forwardingSyncConverged
case i.IsDesired:
return forwardingSyncMissing
default:
return forwardingSyncRuntimeOnly
}
}
func mergeForwardingInventory(
stored []model.ForwardingRule,
runtime []forwarding.Rule,
) ([]forwardingInventoryItem, error) {
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime)) items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
byIdentity := make(map[string]int, len(stored)+len(runtime)) byIdentity := make(map[string]int, len(stored)+len(runtime))
for _, record := range stored { for _, record := range stored {
@@ -474,104 +352,215 @@ func mergeForwardingInventory(
return items, nil return items, nil
} }
func recordForwardingSyncError(err error) { func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
forwardingSyncStateMu.Lock() values := []string{
forwardingLastSyncErr = err item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
forwardingSyncStateMu.Unlock() item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
}
func lastForwardingSyncError() string {
forwardingSyncStateMu.RLock()
defer forwardingSyncStateMu.RUnlock()
if forwardingLastSyncErr == nil {
return ""
} }
return forwardingLastSyncErr.Error() for _, value := range values {
} if strings.Contains(strings.ToLower(value), keyword) {
return true
func applyForwardingOperations(current []forwarding.Rule, requested []dto.ForwardRuleOperation) ([]forwarding.Rule, error) {
desired := make([]forwarding.Rule, 0, len(current)+len(requested))
for _, rule := range current {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, fmt.Errorf("normalize persisted forwarding rule: %w", err)
} }
desired = append(desired, normalized)
} }
for _, operation := range requested { 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, "/") { for _, protocol := range strings.Split(operation.Protocol, "/") {
rule, err := forwarding.NormalizeRule(forwarding.Rule{ rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: operation.Family, Protocol: protocol, Port: operation.Port, TargetIP: operation.TargetIP, Family: operation.Family, Protocol: protocol, Port: operation.Port,
TargetPort: operation.TargetPort, Interface: operation.Interface, TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
}) })
if err != nil { if err != nil {
return nil, err return err
} }
index := forwardingRuleIndex(desired, rule) groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
switch forwarding.OperationType(operation.Operation) { }
case forwarding.OperationAdd: }
if index >= 0 { stored, err := s.rules.List(ctx)
return nil, forwarding.ErrRuleExists if err != nil {
} return err
desired = append(desired, rule) }
case forwarding.OperationRemove: byIdentity := make(map[string]model.ForwardingRule, len(stored))
if index >= 0 { for index, rule := range forwardingRulesFromModels(stored) {
desired = append(desired[:index], desired[index+1:]...) normalized, err := forwarding.NormalizeRule(rule)
} if err != nil {
default: return err
return nil, fmt.Errorf("unsupported forwarding operation %q", operation.Operation) }
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)
} }
} }
} }
return desired, nil 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 {
func forwardingRuleIndex(rules []forwarding.Rule, wanted forwarding.Rule) int { old[rule.Identity()] = true
wantedIdentity := wanted.Identity() }
for index, rule := range rules { unchanged := len(old) == len(groups[1].rules)
if rule.Identity() == wantedIdentity { duplicate := false
return index 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
} }
} }
return -1 var client forwarding.Adapter
} var failures []error
for _, group := range groups {
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool { byFamily := make(map[string][]forwarding.Rule, 2)
if len(operations) == 0 { seen := make(map[string]bool, len(group.rules))
return false for _, rule := range group.rules {
} key := rule.Identity()
for _, operation := range operations { _, exists := byIdentity[key]
if operation.Operation != string(forwarding.OperationRemove) { if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
return false 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 true return errors.Join(failures...)
}
func newForwardingManager() (*forwarding.Manager, error) {
return newForwardingManagerFor(configuredForwardingBackend())
}
func configuredForwardingBackend() string {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
return constant.FirewallProviderIptables
}
return selected
}
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
client, err := lifecycle.NewClientFor(backend)
if err != nil {
return nil, fmt.Errorf(
"%w: selected forwarding backend %s: %w",
errForwardingBackendUnavailable, backend, err,
)
}
adapter, err := forwarding.New(client.Name())
if err != nil {
return nil, err
}
return forwarding.NewManager(adapter, client), nil
} }
+8 -16
View File
@@ -5,7 +5,6 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -325,18 +324,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 +344,9 @@ 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, func(message string) {
if err != nil { taskItem.Log(message)
return err })
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
}, nil) }, nil)
} }
_ = taskItem.Execute() _ = taskItem.Execute()
+37
View File
@@ -0,0 +1,37 @@
package service
import (
"encoding/json"
"errors"
"io"
"strings"
)
// 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 struct {
Stream string `json:"stream"`
Error string `json:"error"`
ErrorDetail struct {
Message string `json:"message"`
} `json:"errorDetail"`
}
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != "" {
return errors.New(message.Error)
}
if message.ErrorDetail.Message != "" {
return errors.New(message.ErrorDetail.Message)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}
+216 -104
View File
@@ -128,115 +128,135 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions { func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions var data dto.MonitorGPUOptions
exist, client := accelerator.New() seen := make(map[string]bool)
if !exist { if exist, client := accelerator.New(); exist {
return data snapshot, err := client.Collect(context.Background())
} if err != nil {
snapshot, err := client.Collect(context.Background()) global.LOG.Warnf("Load accelerator options failed: %v", err)
if err != nil {
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
return data
}
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
}
return loadGPUOptions(snapshot)
}
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 { } else {
hasGPUOrNPU = true data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
} }
} }
switch { devices, err := monitorRepo.GetGPUDevices()
case hasGPUOrNPU && hasXPU: if err != nil {
data.GPUType = "mixed" global.LOG.Warnf("Load accelerator history options failed: %v", err)
case hasXPU: return data
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
} }
for _, device := range devices {
sort.Slice(snapshot.Devices, func(i, j int) bool { key := device.DeviceID
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind { if key == "" {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind key = "legacy:" + device.ProductName
} }
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor { if seen[key] {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor continue
} }
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex { seen[key] = true
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex data.ChartHide = append(data.ChartHide, dto.GPUChartHide{DeviceID: device.DeviceID, ProductName: device.ProductName, Type: device.DeviceType, Legacy: device.DeviceID == ""})
} data.Options = append(data.Options, device.ProductName)
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 data.SampleCount > 1200 {
if gpu.MaxPowerLimit != 0 { seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100) data.BucketSeconds = (seconds + 599) / 600
} else { }
data.PowerPercent = append(data.PowerPercent, float64(0)) points, err := monitorRepo.GetGPUHistory(req.StartTime, data.BucketSeconds, req.Aggregation, opts...)
if err != nil {
return data, err
}
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 {
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal) for i, point := range points {
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed) if i > 0 && points[i-1].IntervalSeconds > 0 {
if gpu.MemTotal != 0 { interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100) if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
} else { samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
data.MemoryPercent = append(data.MemoryPercent, float64(0)) }
}
samples = append(samples, point)
} }
var process []dto.GPUProcess }
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil { for _, point := range samples {
data.ProcessCount = append(data.ProcessCount, len(process)) data.Date = append(data.Date, point.CreatedAt)
data.GPUProcesses = append(data.GPUProcesses, process) data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
} else { data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
data.ProcessCount = append(data.ProcessCount, 0) data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{}) 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.SpeedValue = append(data.SpeedValue, gpu.FanSpeed) data.GPUProcesses = append(data.GPUProcesses, processes)
} }
return data, nil return data, nil
} }
@@ -307,7 +327,6 @@ func (m *MonitorService) CleanData() error {
} }
func (m *MonitorService) Run() { func (m *MonitorService) Run() {
saveAcceleratorDataToDB()
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 {
@@ -343,6 +362,7 @@ func (m *MonitorService) Run() {
m.loadDiskIO() m.loadDiskIO()
m.loadNetIO() m.loadNetIO()
m.saveGPUData()
MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays")) MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays"))
if err != nil { if err != nil {
@@ -602,7 +622,62 @@ 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 (m *MonitorService) saveGPUData() {
status, err := settingRepo.GetValueByKey("MonitorStatus")
if err != nil {
global.LOG.Errorf("load monitor status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
exist, client := accelerator.New() exist, client := accelerator.New()
if !exist { if !exist {
return return
@@ -615,27 +690,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("MonitorInterval")); 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{
ProductName: device.Label, MemoryUtil: device.Metrics.MemoryUtil.Value,
GPUUtil: device.Metrics.Utilization.ValueOrZero(), MemoryActivity: device.Metrics.MemoryActivity.Value,
Temperature: device.Metrics.Temperature.ValueOrZero(), EncoderUtil: device.Metrics.EncoderUtil.Value,
PowerDraw: device.Metrics.Power.ValueOrZero(), DecoderUtil: device.Metrics.DecoderUtil.Value,
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(), JPEGUtil: device.Metrics.JPEGUtil.Value,
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(), OFAUtil: device.Metrics.OFAUtil.Value,
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(), MediaUtil: device.Metrics.MediaUtil.Value,
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()), 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,
DeviceID: device.ID,
DeviceType: string(device.Kind),
ProcessStatus: device.ProcessStatus,
Frequency: device.Metrics.Frequency.Value,
MemoryFrequency: device.Metrics.MemoryFrequency.Value,
MemoryTemperature: device.Metrics.MemoryTemperature.Value,
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))
+1 -1
View File
@@ -228,7 +228,7 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err return err
} }
if req.Key == "Port" { if req.Key == "Port" {
if err := updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil { if err := newFirewallService().updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil { if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr) return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
} }
+4 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"path" "path"
"strconv" "strconv"
@@ -32,7 +33,9 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err return nil, err
} }
go func() { go func() {
_ = ensureFirewallPorts(addPorts) if err := ensureFirewallPorts(addPorts); err != nil {
global.LOG.Errorf("allow website firewall ports failed: %v", err)
}
}() }()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
+12 -2
View File
@@ -154,7 +154,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log") logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
logger := logrus.New() logger := logrus.New()
logger.SetFormatter(&SimpleFormatter{}) logger.SetFormatter(&SimpleFormatter{})
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm) logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err) return nil, fmt.Errorf("failed to open log file: %w", err)
} }
@@ -283,8 +283,18 @@ func (t *Task) updateTask(task *model.Task) {
_ = t.taskRepo.Update(context.Background(), task) _ = t.taskRepo.Update(context.Background(), task)
} }
func (t *Task) Execute() error { // Prepare makes a task visible before dispatching it to a background worker.
func (t *Task) Prepare() error {
if err := t.taskRepo.Save(context.Background(), t.Task); err != nil { if err := t.taskRepo.Save(context.Background(), t.Task); err != nil {
_ = t.logFile.Close()
global.RemoveTaskCancel(t.TaskID)
return err
}
return nil
}
func (t *Task) Execute() error {
if err := t.Prepare(); err != nil {
return err return err
} }
var err error var err error
+20 -4
View File
@@ -644,6 +644,13 @@
"formatZH": "创建容器网络 name", "formatZH": "创建容器网络 name",
"formatEN": "create container network [name]" "formatEN": "create container network [name]"
}, },
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": { "/containers/network/del": {
"bodyKeys": [ "bodyKeys": [
"names" "names"
@@ -4028,12 +4035,21 @@
}, },
"/runtimes/operate": { "/runtimes/operate": {
"bodyKeys": [ "bodyKeys": [
"id" "ID"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [
"formatZH": "操作运行环境 [id]", {
"formatEN": "Operate runtime [id]" "input_column": "id",
"input_value": "ID",
"isList": false,
"db": "runtimes",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "操作运行环境 [name]",
"formatEN": "Operate runtime [name]"
}, },
"/runtimes/php/config": { "/runtimes/php/config": {
"bodyKeys": [ "bodyKeys": [
+22
View File
@@ -649,6 +649,7 @@ CommonAlert: "Panel {{ .node }}{{ .ip }}: {{ .msg }}. Log in to view details."
NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details." NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details."
LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details." LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details."
SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details." SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details."
CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details."
# disk # disk
DeviceNotFound: "Device {{ .name }} not found" DeviceNotFound: "Device {{ .name }} not found"
@@ -719,3 +720,24 @@ FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule." ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list" FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules" FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
FileTaskCopy: 'Copy files to {{ .dst }}'
FileTaskMove: 'Move files to {{ .dst }}'
FileTaskCompress: 'Compress files to {{ .dst }}'
FileTaskDecompress: 'Extract files to {{ .dst }}'
FileTaskSource: 'Source path: {{ .path }}'
FileTaskFormat: 'Archive format: {{ .format }}'
FileTaskRename: 'Target filename: {{ .name }}'
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})"
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network"
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network"
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed"
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted"
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .msg }}. Inicie sesión en el pa
NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.' NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.'
LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.' LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.'
SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.' SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.'
# disco # disco
DeviceNotFound: 'Dispositivo {{ .name }} no encontrado' DeviceNotFound: 'Dispositivo {{ .name }} no encontrado'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva." ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista" FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema" FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"
FileTaskCopy: 'Copiar archivos a {{ .dst }}'
FileTaskMove: 'Mover archivos a {{ .dst }}'
FileTaskCompress: 'Comprimir archivos en {{ .dst }}'
FileTaskDecompress: 'Extraer archivos a {{ .dst }}'
FileTaskSource: 'Ruta de origen: {{ .path }}'
FileTaskFormat: 'Formato del archivo: {{ .format }}'
FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
+13
View File
@@ -649,6 +649,8 @@ CommonAlert: "پنل {{ .node }}{{ .ip }}: {{ .msg }}. برای مشاهده ج
NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید." NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید."
LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید." LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید."
SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید." SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید."
CronJobSuccessAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی‌شده {{ .name }} با موفقیت تکمیل شد. برای مشاهده جزئیات وارد شوید."
# دیسک # دیسک
DeviceNotFound: "دستگاه {{ .name }} یافت نشد" DeviceNotFound: "دستگاه {{ .name }} یافت نشد"
@@ -719,3 +721,14 @@ FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Doc
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید." ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید" FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود" FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}'
FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}'
FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}'
FileTaskDecompress: 'استخراج فایل‌ها در {{ .dst }}'
FileTaskSource: 'مسیر مبدأ: {{ .path }}'
FileTaskFormat: 'قالب بایگانی: {{ .format }}'
FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .msg }}。詳細
NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして' NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして'
LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして' LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして'
SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして' SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして'
CronJobSuccessAlert: 'パネル {{ .node }}{{ .ip }}: スケジュールタスク {{ .name }} が正常に完了しました。詳細はパネルにログインして確認してください。'
# ディスク # ディスク
DeviceNotFound: 'デバイス {{ .name }} が見つかりません' DeviceNotFound: 'デバイス {{ .name }} が見つかりません'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。" ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください" FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています" FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
FileTaskCopy: 'ファイルを {{ .dst }} にコピー'
FileTaskMove: 'ファイルを {{ .dst }} に移動'
FileTaskCompress: 'ファイルを {{ .dst }} に圧縮'
FileTaskDecompress: 'ファイルを {{ .dst }} に展開'
FileTaskSource: '元のパス:{{ .path }}'
FileTaskFormat: '圧縮形式:{{ .format }}'
FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .msg }}。자세한 내
NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
CronJobSuccessAlert: '패널 {{ .node }}{{ .ip }}: 예약 작업 {{ .name }}이(가) 성공적으로 완료되었습니다. 로그인하여 자세한 내용을 확인하십시오.'
# 디스크 # 디스크
DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다' DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요." ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요" FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다" FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
FileTaskCopy: '{{ .dst }}에 파일 복사'
FileTaskMove: '{{ .dst }}로 파일 이동'
FileTaskCompress: '{{ .dst }}에 파일 압축'
FileTaskDecompress: '{{ .dst }}에 압축 해제'
FileTaskSource: '원본 경로: {{ .path }}'
FileTaskFormat: '압축 형식: {{ .format }}'
FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
+12
View File
@@ -639,6 +639,7 @@ CommonAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: {{ .msg }}. ເ
NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
CronJobSuccessAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ສຳເລັດແລ້ວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
#disk #disk
DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}" DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}"
@@ -710,3 +711,14 @@ FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອ
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່." ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ" FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ" FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}'
FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}'
FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .msg }}. Sila log masuk ke pan
NodeExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} nod tidak normal. Sila log masuk ke panel untuk butiran lanjut.' NodeExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} nod tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
LicenseExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} lesen tidak normal. Sila log masuk ke panel untuk butiran lanjut.' LicenseExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} lesen tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
SSHAndPanelLoginAlert: 'Panel {{ .node }}{{ .ip }} Anda, log masuk {{ .name }} dari {{ .loginIp }} tidak normal. Sila log masuk ke panel untuk butiran lanjut.' SSHAndPanelLoginAlert: 'Panel {{ .node }}{{ .ip }} Anda, log masuk {{ .name }} dari {{ .loginIp }} tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: tugas berjadual {{ .name }} berjaya diselesaikan. Log masuk untuk melihat butiran.'
# cakera # cakera
DeviceNotFound: 'Peranti {{ .name }} tidak ditemui' DeviceNotFound: 'Peranti {{ .name }} tidak ditemui'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu." ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan" FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem" FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
FileTaskCopy: 'Salin fail ke {{ .dst }}'
FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
FileTaskDecompress: 'Ekstrak fail ke {{ .dst }}'
FileTaskSource: 'Laluan sumber: {{ .path }}'
FileTaskFormat: 'Format arkib: {{ .format }}'
FileTaskRename: 'Nama fail sasaran: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker."
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .msg }}. Faça login no painel
NodeExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} nós estão anormais. Faça login no painel para obter detalhes.' NodeExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} nós estão anormais. Faça login no painel para obter detalhes.'
LicenseExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} licenças estão anormais. Faça login no painel para obter detalhes.' LicenseExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} licenças estão anormais. Faça login no painel para obter detalhes.'
SSHAndPanelLoginAlert: 'Seu Painel {{ .node }}{{ .ip }}, o login {{ .name }} a partir de {{ .loginIp }} é anormal. Faça login no painel para obter detalhes.' SSHAndPanelLoginAlert: 'Seu Painel {{ .node }}{{ .ip }}, o login {{ .name }} a partir de {{ .loginIp }} é anormal. Faça login no painel para obter detalhes.'
CronJobSuccessAlert: 'Painel {{ .node }}{{ .ip }}: a tarefa agendada {{ .name }} foi concluída com sucesso. Faça login para ver os detalhes.'
# disco # disco
DeviceNotFound: 'Dispositivo {{ .name }} não encontrado' DeviceNotFound: 'Dispositivo {{ .name }} não encontrado'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Dock
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra." ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista" FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema" FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"
FileTaskCopy: 'Copiar arquivos para {{ .dst }}'
FileTaskMove: 'Mover arquivos para {{ .dst }}'
FileTaskCompress: 'Compactar arquivos em {{ .dst }}'
FileTaskDecompress: 'Extrair arquivos para {{ .dst }}'
FileTaskSource: 'Caminho de origem: {{ .path }}'
FileTaskFormat: 'Formato do arquivo: {{ .format }}'
FileTaskRename: 'Nome do arquivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker."
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .msg }}. Войдит
NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.' NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.'
LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.' LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.'
SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.' SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.'
CronJobSuccessAlert: 'Панель {{ .node }}{{ .ip }}: запланированная задача {{ .name }} успешно завершена. Войдите для просмотра подробностей.'
# диск # диск
DeviceNotFound: 'Устройство {{ .name }} не найдено' DeviceNotFound: 'Устройство {{ .name }} не найдено'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Сохранить состояние защи
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило." ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил" FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов" FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
FileTaskCopy: 'Копирование файлов в {{ .dst }}'
FileTaskMove: 'Перемещение файлов в {{ .dst }}'
FileTaskCompress: 'Сжатие файлов в {{ .dst }}'
FileTaskDecompress: 'Распаковка файлов в {{ .dst }}'
FileTaskSource: 'Исходный путь: {{ .path }}'
FileTaskFormat: 'Формат архива: {{ .format }}'
FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
+12
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .msg }}. Detaylar için panelin
NodeExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} düğüm anormal durumda. Detaylar için paneline giriş yapın.' NodeExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} düğüm anormal durumda. Detaylar için paneline giriş yapın.'
LicenseExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} lisans anormal durumda. Detaylar için paneline giriş yapın.' LicenseExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} lisans anormal durumda. Detaylar için paneline giriş yapın.'
SSHAndPanelLoginAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .loginIp }} adresinden {{ .name }} girişi anormal. Detaylar için paneline giriş yapın.' SSHAndPanelLoginAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .loginIp }} adresinden {{ .name }} girişi anormal. Detaylar için paneline giriş yapın.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: zamanlanmış görev {{ .name }} başarıyla tamamlandı. Ayrıntıları görmek için giriş yapın.'
# disk # disk
DeviceNotFound: 'Cihaz {{ .name }} bulunamadı' DeviceNotFound: 'Cihaz {{ .name }} bulunamadı'
@@ -719,3 +720,14 @@ FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kayd
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun." ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin" FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor" FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"
FileTaskCopy: 'Dosyaları {{ .dst }} konumuna kopyala'
FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı'
FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır'
FileTaskDecompress: 'Dosyaları {{ .dst }} konumuna çıkar'
FileTaskSource: 'Kaynak yol: {{ .path }}'
FileTaskFormat: 'Arşiv biçimi: {{ .format }}'
FileTaskRename: 'Hedef dosya adı: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin."
ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın."
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
+22
View File
@@ -649,6 +649,7 @@ CommonAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},詳情請登入
NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。' NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。'
LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。' LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。'
SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。' SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。'
CronJobSuccessAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務 {{ .name }} 執行成功,詳情請登入面板檢視。'
# 磁碟 # 磁碟
DeviceNotFound: '裝置 {{ .name }} 未找到' DeviceNotFound: '裝置 {{ .name }} 未找到'
@@ -719,3 +720,24 @@ FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。" ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除" FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護" FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
FileTaskCopy: '複製檔案至 {{ .dst }}'
FileTaskMove: '移動檔案至 {{ .dst }}'
FileTaskCompress: '壓縮檔案至 {{ .dst }}'
FileTaskDecompress: '解壓檔案至 {{ .dst }}'
FileTaskSource: '來源路徑:{{ .path }}'
FileTaskFormat: '壓縮格式:{{ .format }}'
FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除"
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除"
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除"
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在"
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除"
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
+22
View File
@@ -649,6 +649,7 @@ CommonAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},请登录面板
NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。" NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。"
LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。" LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。"
SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。" SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。"
CronJobSuccessAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行成功,请登录面板查看详情。"
# 磁盘 # 磁盘
DeviceNotFound: "设备 {{ .name }} 未找到" DeviceNotFound: "设备 {{ .name }} 未找到"
@@ -719,3 +720,24 @@ FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。" ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除" FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护" FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
FileTaskCopy: '复制文件到 {{ .dst }}'
FileTaskMove: '移动文件到 {{ .dst }}'
FileTaskCompress: '压缩文件到 {{ .dst }}'
FileTaskDecompress: '解压文件到 {{ .dst }}'
FileTaskSource: '源路径:{{ .path }}'
FileTaskFormat: '压缩格式:{{ .format }}'
FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除"
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除"
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除"
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在"
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除"
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
+5 -3
View File
@@ -103,10 +103,12 @@ func repairIptablesBaseChains(clientName string) {
if status != constant.StatusEnable { if status != constant.StatusEnable {
return return
} }
manager := iptables_helper.Manager{ ports, err := service.LoadRequiredFirewallPortWhiteList()
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList, if err != nil {
global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err)
return
} }
if err := manager.RepairBaseChains(); err != nil { if err := iptables_helper.RepairBaseChains(ports); err != nil {
global.LOG.Warnf("repair iptables base chains failed, err: %v", err) global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
} }
} }
+1
View File
@@ -111,6 +111,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.SimplifyFirewallRulePolicy, migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly, migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources, migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
} }
} }
@@ -162,6 +162,9 @@ func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error
for _, rule := range defaults { for _, rule := range defaults {
index, found := indexes[key(rule)] index, found := indexes[key(rule)]
if !found { if !found {
if rule.Type == "" {
continue
}
index = len(rules) index = len(rules)
indexes[key(rule)] = index indexes[key(rule)] = index
rules = append(rules, rule) rules = append(rules, rule)
+7
View File
@@ -1908,3 +1908,10 @@ var AddDockerPortGuardReadOnly = &gormigrate.Migration{
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
}, },
} }
var AddAcceleratorMetrics = &gormigrate.Migration{
ID: "20260928-accelerator-vendor-metrics",
Migrate: func(tx *gorm.DB) error {
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
@@ -2,6 +2,9 @@ package utils
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
@@ -142,7 +145,7 @@ func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provid
} }
continue continue
} }
identity := item.PolicyKey() identity := hostFirewallPolicyKey(item)
if index, exists := byIdentity[identity]; exists { if index, exists := byIdentity[identity]; exists {
if item.Description != "" { if item.Description != "" {
converted[index].Description = item.Description converted[index].Description = item.Description
@@ -336,10 +339,30 @@ func legacyIPOrPrefix(value string) bool {
} }
func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) { func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) {
record, err := model.FirewallRuleFromDomain(rule) normalized, err := filter.NormalizeRule(rule)
if err != nil { if err != nil {
return model.FirewallRule{}, err return model.FirewallRule{}, err
} }
switch normalized.NativeKind {
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
default:
return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
}
record := model.FirewallRule{
Family: string(normalized.Scope.Family),
Protocol: normalized.Protocol,
SourceAddress: normalized.SourceAddress,
SourcePort: normalized.SourcePort,
DestinationAddress: normalized.DestinationAddress,
DestinationPort: normalized.DestinationPort,
Interface: normalized.Interface,
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
Action: string(normalized.Action),
Description: normalized.Description,
}
if normalized.Scope.Provider == filter.ProviderFirewalld {
record.Priority = normalized.Priority
}
record.UUID = uuid.NewString() record.UUID = uuid.NewString()
record.Origin = constant.FirewallRuleOriginAdopted record.Origin = constant.FirewallRuleOriginAdopted
record.Owner = constant.FirewallRuleSourceUser record.Owner = constant.FirewallRuleSourceUser
@@ -347,6 +370,27 @@ func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error)
return record, nil return record, nil
} }
func hostFirewallPolicyKey(rule model.FirewallRule) string {
payload, _ := json.Marshal(struct {
Family string `json:"family"`
Protocol string `json:"protocol"`
SourceAddress string `json:"sourceAddress,omitempty"`
SourcePort string `json:"sourcePort,omitempty"`
DestinationAddress string `json:"destinationAddress,omitempty"`
DestinationPort string `json:"destinationPort,omitempty"`
Interface string `json:"interface,omitempty"`
ConnectionStates string `json:"connectionStates,omitempty"`
Action string `json:"action"`
}{
Family: rule.Family, Protocol: rule.Protocol,
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
})
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:])
}
func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error { func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error {
var existing []model.FirewallRule var existing []model.FirewallRule
if err := tx.Find(&existing).Error; err != nil { if err := tx.Find(&existing).Error; err != nil {
@@ -354,10 +398,10 @@ func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) erro
} }
byIdentity := make(map[string]model.FirewallRule, len(existing)) byIdentity := make(map[string]model.FirewallRule, len(existing))
for _, item := range existing { for _, item := range existing {
byIdentity[item.PolicyKey()] = item byIdentity[hostFirewallPolicyKey(item)] = item
} }
for _, item := range rules { for _, item := range rules {
identity := item.PolicyKey() identity := hostFirewallPolicyKey(item)
if current, exists := byIdentity[identity]; exists { if current, exists := byIdentity[identity]; exists {
if current.Description == "" && item.Description != "" { if current.Description == "" && item.Description != "" {
if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID). if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID).
+1
View File
@@ -77,6 +77,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.GET("/network", baseApi.ListNetwork) baRouter.GET("/network", baseApi.ListNetwork)
baRouter.POST("/network/del", baseApi.DeleteNetwork) baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/clean", baseApi.CleanNetworks)
baRouter.POST("/network/search", baseApi.SearchNetwork) baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork) baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume) baRouter.GET("/volume", baseApi.ListVolume)
+1
View File
@@ -43,6 +43,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus) cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf) cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli) cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.GET("/redis/cli/status", baseApi.LoadRedisCliStatus)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli) cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf) cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf)
+1
View File
@@ -11,6 +11,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts") hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi baseApi := v2.ApiGroupApp.BaseApi
Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions) Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions)
Router.GET("/internal/terminal/capabilities", baseApi.TerminalCapabilities)
{ {
hostRouter.POST("", baseApi.CreateHost) hostRouter.POST("", baseApi.CreateHost)
hostRouter.POST("/info", baseApi.GetHostByID) hostRouter.POST("/info", baseApi.GetHostByID)
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -52,6 +53,11 @@ func (c Client) LoadInfo() (*Info, error) {
} }
func (c Client) Collect(ctx context.Context) (*Snapshot, error) { func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
results := make([]providerResult, len(c.providers)) results := make([]providerResult, len(c.providers))
var wg sync.WaitGroup var wg sync.WaitGroup
for index, item := range c.providers { for index, item := range c.providers {
@@ -66,8 +72,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
}() }()
} }
wg.Wait() wg.Wait()
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot := &Snapshot{DriverVersions: make(map[string]string)} snapshot := &Snapshot{}
var ( var (
errs []error errs []error
active []*ProviderSnapshot active []*ProviderSnapshot
@@ -78,6 +87,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
errs = append(errs, result.err) errs = append(errs, result.err)
continue continue
} }
if result.snapshot != nil {
for _, warning := range result.snapshot.Warnings {
errs = append(errs, errors.New(warning))
}
}
if result.snapshot == nil || len(result.snapshot.Devices) == 0 { if result.snapshot == nil || len(result.snapshot.Devices) == 0 {
continue continue
} }
@@ -90,11 +104,8 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if item.CudaVersion != "" { if item.CudaVersion != "" {
snapshot.Info.CudaVersion = item.CudaVersion snapshot.Info.CudaVersion = item.CudaVersion
} }
if item.DriverVersion != "" { if item.Type == "xpu" {
snapshot.DriverVersions[item.Type] = item.DriverVersion xpuVersion = item.DriverVersion
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
} }
} }
snapshot.Warnings = append(snapshot.Warnings, errs...) snapshot.Warnings = append(snapshot.Warnings, errs...)
@@ -102,6 +113,10 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
return nil, fmt.Errorf("calling accelerator monitoring tools failed: %w", errors.Join(errs...)) return nil, fmt.Errorf("calling accelerator monitoring tools failed: %w", errors.Join(errs...))
} }
snapshot.Info.CollectedAt = time.Now()
for _, err := range errs {
snapshot.Info.Warnings = append(snapshot.Info.Warnings, err.Error())
}
snapshot.Info.XPUDriverVersion = xpuVersion snapshot.Info.XPUDriverVersion = xpuVersion
snapshot.Info.Type, snapshot.Info.DriverVersion = mergeProviderMetadata(active) snapshot.Info.Type, snapshot.Info.DriverVersion = mergeProviderMetadata(active)
return snapshot, nil return snapshot, nil
+3 -2
View File
@@ -1,6 +1,7 @@
package accelerator package accelerator
import ( import (
"math"
"strconv" "strconv"
"strings" "strings"
@@ -18,11 +19,11 @@ func metric(display, unit string) Metric {
return result return result
} }
parsed, err := strconv.ParseFloat(matched[1], 64) parsed, err := strconv.ParseFloat(matched[1], 64)
if err != nil { if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || (parsed < 0 && unit != "°C") {
return result return result
} }
normalized, ok := convertMetricUnit(parsed, matched[2], unit) normalized, ok := convertMetricUnit(parsed, matched[2], unit)
if !ok { if !ok || math.IsNaN(normalized) || math.IsInf(normalized, 0) || (unit == "%" && normalized > 100) {
return result return result
} }
result.Value = &normalized result.Value = &normalized
+79 -63
View File
@@ -12,26 +12,38 @@ import (
func normalizeGPU(item *gpu.Device) Device { func normalizeGPU(item *gpu.Device) Device {
metrics := Metrics{ metrics := Metrics{
Utilization: metric(item.GPUUtil, "%"), MemoryActivity: metric(item.MemoryActivity, "%"),
Temperature: metric(item.Temperature, "°C"), EncoderUtil: metric(item.EncoderUtil, "%"),
Power: metric(item.PowerDraw, "W"), DecoderUtil: metric(item.DecoderUtil, "%"),
PowerLimit: metric(item.MaxPowerLimit, "W"), JPEGUtil: metric(item.JPEGUtil, "%"),
MemoryUsed: memoryMetric(item.MemUsed), OFAUtil: metric(item.OFAUtil, "%"),
MemoryTotal: memoryMetric(item.MemTotal), MediaUtil: metric(item.MediaUtil, "%"),
FanSpeed: metric(item.FanSpeed, "%"), HotspotTemperature: metric(item.HotspotTemperature, "°C"),
FanRPM: metric(item.FanRPM, "RPM"),
MediaFrequency: metric(item.MediaFrequency, "MHz"),
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
MemoryTemperature: metric(item.MemoryTemperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.PowerLimit, "W"),
Frequency: metric(item.Frequency, "MHz"),
MemoryFrequency: metric(item.MemoryFrequency, "MHz"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
} }
device := Device{ device := Device{
ID: stableID(item.Type, item.BusID, strconv.FormatUint(uint64(item.Index), 10)), ProcessStatus: item.ProcessStatus,
Kind: KindGPU, ID: stableID(item.Type, item.UUID, stableID("pci", item.BusID, strconv.FormatUint(uint64(item.Index), 10))),
Vendor: item.Type, Kind: KindGPU,
Index: int(item.Index), Vendor: item.Type,
Name: item.ProductName, Index: int(item.Index),
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName), Name: item.ProductName,
BusID: item.BusID, Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
Metrics: metrics, BusID: item.BusID,
GPU: item, Metrics: metrics,
GPU: item,
} }
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes { for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{ device.Processes = append(device.Processes, Process{
PID: process.PID, PID: process.PID,
@@ -45,26 +57,36 @@ func normalizeGPU(item *gpu.Device) Device {
func normalizeNPU(item *npu.Device) Device { func normalizeNPU(item *npu.Device) Device {
metrics := Metrics{ metrics := Metrics{
Utilization: metric(item.AICore, "%"), AICPUUtil: metric(item.AICPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"), CtrlCPUUtil: metric(item.CtrlCPUUtil, "%"),
Power: metric(item.PowerDraw, "W"), DDRBandwidth: metric(item.DDRBandwidth, "%"),
MemoryUsed: memoryMetric(item.MemUsed), HBMBandwidth: metric(item.HBMBandwidth, "%"),
MemoryTotal: memoryMetric(item.MemTotal), DDRUsed: metric(item.MemoryUsed, "MiB"),
DDRTotal: metric(item.MemoryTotal, "MiB"),
HBMUsed: metric(item.HBMUsed, "MiB"),
HBMTotal: metric(item.HBMTotal, "MiB"),
HugepagesUsed: metric(item.HugepagesUsed, "pages"),
HugepagesTotal: metric(item.HugepagesTotal, "pages"),
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
} }
device := Device{ device := Device{
ID: fmt.Sprintf("ascend:%d:%d", item.NPUIndex, item.ChipIndex), ProcessStatus: item.ProcessStatus,
Kind: KindNPU, ID: fmt.Sprintf("%s:%d", stableID("ascend", item.BusID, strconv.FormatUint(uint64(item.NPUIndex), 10)), item.ChipIndex),
Vendor: "ascend", Kind: KindNPU,
Index: int(item.Index), Vendor: "ascend",
NPUIndex: int(item.NPUIndex), Index: int(item.Index),
ChipIndex: int(item.ChipIndex), NPUIndex: int(item.NPUIndex),
Name: item.ProductName, ChipIndex: int(item.ChipIndex),
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName), Name: item.ProductName,
BusID: item.BusID, Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
Metrics: metrics, BusID: item.BusID,
NPU: item, Metrics: metrics,
NPU: item,
} }
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes { for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{ device.Processes = append(device.Processes, Process{
PID: process.PID, PID: process.PID,
@@ -78,26 +100,32 @@ func normalizeNPU(item *npu.Device) Device {
func normalizeXPU(item *xpu.Device) Device { func normalizeXPU(item *xpu.Device) Device {
metrics := Metrics{ metrics := Metrics{
Utilization: metric(item.Stats.GPUUtil, "%"), MediaUtil: metric(item.Stats.MediaUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"), ComputeUtil: metric(item.Stats.ComputeUtil, "%"),
Power: metric(item.Stats.Power, "W"), CopyUtil: metric(item.Stats.CopyUtil, "%"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed), MediaFrequency: metric(item.Stats.MediaFrequency, "MHz"),
MemoryTotal: memoryMetric(item.Basic.Memory), MemoryTemperature: metric(item.Stats.MemoryTemperature, "°C"),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"), MemoryBandwidth: metric(item.Stats.MemoryBandwidthUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"), Utilization: metric(item.Stats.GPUUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"),
Power: metric(item.Stats.Power, "W"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed),
MemoryTotal: memoryMetric(item.Basic.Memory),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"),
} }
device := Device{ device := Device{
ID: stableID("xpu", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID)), ProcessStatus: item.ProcessStatus,
Kind: KindXPU, ID: stableID("xpu", item.Basic.UUID, stableID("pci", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID))),
Vendor: item.Basic.VendorName, Kind: KindXPU,
Index: item.Basic.DeviceID, Vendor: item.Basic.VendorName,
Name: item.Basic.DeviceName, Index: item.Basic.DeviceID,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName), Name: item.Basic.DeviceName,
BusID: item.Basic.PciBdfAddress, Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
Metrics: metrics, BusID: item.Basic.PciBdfAddress,
XPU: item, Metrics: metrics,
XPU: item,
} }
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes { for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{ device.Processes = append(device.Processes, Process{
PID: strconv.Itoa(process.PID), PID: strconv.Itoa(process.PID),
@@ -110,18 +138,6 @@ func normalizeXPU(item *xpu.Device) Device {
return device return device
} }
func capabilities(metrics Metrics) Capabilities {
return Capabilities{
Utilization: metrics.Utilization.Available(),
Temperature: metrics.Temperature.Available(),
Power: metrics.Power.Available(),
PowerLimit: metrics.PowerLimit.Available(),
Memory: metrics.MemoryUsed.Available() || metrics.MemoryTotal.Available(),
FanSpeed: metrics.FanSpeed.Available(),
Frequency: metrics.Frequency.Available(),
}
}
func stableID(vendor, busID, fallback string) string { func stableID(vendor, busID, fallback string) string {
if busID != "" && !strings.EqualFold(busID, "N/A") { if busID != "" && !strings.EqualFold(busID, "N/A") {
return vendor + ":" + busID return vendor + ":" + busID
+17 -1
View File
@@ -2,6 +2,7 @@ package accelerator
import ( import (
"context" "context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -26,6 +27,7 @@ func (p gpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
} }
result := &ProviderSnapshot{ result := &ProviderSnapshot{
Type: info.Type, Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion, DriverVersion: info.DriverVersion,
CudaVersion: info.CudaVersion, CudaVersion: info.CudaVersion,
GPUs: info.Devices, GPUs: info.Devices,
@@ -49,6 +51,7 @@ func (p npuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
} }
result := &ProviderSnapshot{ result := &ProviderSnapshot{
Type: info.Type, Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion, DriverVersion: info.DriverVersion,
NPUs: info.Devices, NPUs: info.Devices,
} }
@@ -71,11 +74,24 @@ func (p xpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
} }
result := &ProviderSnapshot{ result := &ProviderSnapshot{
Type: info.Type, Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion, DriverVersion: info.DriverVersion,
XPUs: info.Devices, XPUs: info.Devices,
} }
for index := range result.XPUs { for index := range result.XPUs {
result.Devices = append(result.Devices, normalizeXPU(&result.XPUs[index]))
parent := normalizeXPU(&result.XPUs[index])
result.Devices = append(result.Devices, parent)
for _, tile := range result.XPUs[index].Tiles {
raw := xpu.Device{Basic: result.XPUs[index].Basic, Stats: tile.Stats, ProcessStatus: "unavailable"}
raw.Basic.Memory = ""
raw.Basic.FreeMemory = ""
device := normalizeXPU(&raw)
device.ParentID = parent.ID
device.ID = fmt.Sprintf("%s:tile:%d", parent.ID, tile.TileID)
device.Label = fmt.Sprintf("%s / Tile %d", parent.Label, tile.TileID)
result.Devices = append(result.Devices, device)
}
} }
return result, nil return result, nil
} }
+54 -34
View File
@@ -2,6 +2,7 @@ package accelerator
import ( import (
"errors" "errors"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -17,6 +18,9 @@ const (
) )
type Info struct { type Info struct {
CollectedAt time.Time `json:"collectedAt"`
Warnings []string `json:"warnings"`
Type string `json:"type"` Type string `json:"type"`
CudaVersion string `json:"cudaVersion"` CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"` DriverVersion string `json:"driverVersion"`
@@ -44,25 +48,40 @@ func (m Metric) ValueOrZero() float64 {
} }
type Metrics struct { type Metrics struct {
Utilization Metric MemoryActivity Metric
Temperature Metric EncoderUtil Metric
Power Metric DecoderUtil Metric
PowerLimit Metric JPEGUtil Metric
MemoryUsed Metric OFAUtil Metric
MemoryTotal Metric MediaUtil Metric
MemoryUtil Metric ComputeUtil Metric
FanSpeed Metric CopyUtil Metric
Frequency Metric HotspotTemperature Metric
} FanRPM Metric
AICPUUtil Metric
CtrlCPUUtil Metric
DDRUsed Metric
DDRTotal Metric
HBMUsed Metric
HBMTotal Metric
DDRBandwidth Metric
HBMBandwidth Metric
MemoryBandwidth Metric
MediaFrequency Metric
HugepagesUsed Metric
HugepagesTotal Metric
type Capabilities struct { MemoryTemperature Metric
Utilization bool Utilization Metric
Temperature bool Temperature Metric
Power bool Power Metric
PowerLimit bool PowerLimit Metric
Memory bool MemoryUsed Metric
FanSpeed bool MemoryTotal Metric
Frequency bool MemoryUtil Metric
FanSpeed Metric
Frequency Metric
MemoryFrequency Metric
} }
type Process struct { type Process struct {
@@ -74,18 +93,19 @@ type Process struct {
} }
type Device struct { type Device struct {
ID string ParentID string
Kind Kind ProcessStatus string
Vendor string ID string
Index int Kind Kind
NPUIndex int Vendor string
ChipIndex int Index int
Name string NPUIndex int
Label string ChipIndex int
BusID string Name string
Metrics Metrics Label string
Capabilities Capabilities BusID string
Processes []Process Metrics Metrics
Processes []Process
GPU *gpu.Device `json:"-"` GPU *gpu.Device `json:"-"`
NPU *npu.Device `json:"-"` NPU *npu.Device `json:"-"`
@@ -93,10 +113,9 @@ type Device struct {
} }
type Snapshot struct { type Snapshot struct {
Info Info Info Info
Devices []Device Devices []Device
DriverVersions map[string]string Warnings []error
Warnings []error
} }
func (s Snapshot) Warning() error { func (s Snapshot) Warning() error {
@@ -104,6 +123,7 @@ func (s Snapshot) Warning() error {
} }
type ProviderSnapshot struct { type ProviderSnapshot struct {
Warnings []string
Type string Type string
DriverVersion string DriverVersion string
CudaVersion string CudaVersion string
+58 -17
View File
@@ -3,12 +3,13 @@ package gpu
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
) )
@@ -32,16 +33,22 @@ func findAMDSMI() (string, bool) {
func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) { func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
var ( var (
staticData string staticData string
metricData string metricData string
processData string extendedData string
staticErr error extendedErr error
metricErr error processData string
processErr error staticErr error
wg sync.WaitGroup metricErr error
processErr error
wg sync.WaitGroup
) )
wg.Add(3) wg.Add(4)
go func() {
defer wg.Done()
extendedData, extendedErr = runAMDSMI(ctx, a.command, "metric", "--clock", "--ecc", "--json")
}()
go func() { go func() {
defer wg.Done() defer wg.Done()
staticData, staticErr = runAMDSMI(ctx, a.command, "static", "--asic", "--bus", "--driver", "--limit", "--json") staticData, staticErr = runAMDSMI(ctx, a.command, "static", "--asic", "--bus", "--driver", "--limit", "--json")
@@ -64,16 +71,25 @@ func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
return nil, fmt.Errorf("parsing %s static output failed: %w", a.command, err) return nil, fmt.Errorf("parsing %s static output failed: %w", a.command, err)
} }
if metricErr == nil {
metricErr = applyAMDMetrics(info, metricData)
}
if metricErr != nil { if metricErr != nil {
global.LOG.Warnf("calling %s metric failed, metrics will be omitted: %v", a.command, metricErr) info.Warnings = append(info.Warnings, fmt.Sprintf("%s metrics: %v", a.command, metricErr))
} else if err := applyAMDMetrics(info, metricData); err != nil { }
global.LOG.Warnf("parsing %s metric output failed, metrics will be omitted: %v", a.command, err) if extendedErr == nil {
extendedErr = applyAMDMetrics(info, extendedData)
}
if extendedErr != nil {
info.Warnings = append(info.Warnings, fmt.Sprintf("%s extended metrics: %v", a.command, extendedErr))
}
if processErr == nil {
processErr = applyAMDProcesses(info, processData)
} }
if processErr != nil { if processErr != nil {
global.LOG.Warnf("calling %s process failed, process information will be omitted: %v", a.command, processErr) info.Warnings = append(info.Warnings, fmt.Sprintf("%s processes: %v", a.command, processErr))
} else if err := applyAMDProcesses(info, processData); err != nil {
global.LOG.Warnf("parsing %s process output failed, process information will be omitted: %v", a.command, err)
} }
return info, nil return info, nil
} }
@@ -96,6 +112,9 @@ func parseAMDStatic(data string) (*Info, error) {
} }
device := Device{ device := Device{
Type: "amd", Type: "amd",
UUID: amdStringAt(row, "uuid", "asic.uuid"),
DriverVersion: amdStringAt(row, "driver.version", "driver_version", "amdgpu_version"),
ProcessStatus: "unavailable",
Index: index, Index: index,
ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"), ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"),
PersistenceMode: "N/A", PersistenceMode: "N/A",
@@ -111,6 +130,7 @@ func parseAMDStatic(data string) (*Info, error) {
"limit.max_power_limit", "limit.max_power_limit",
"limit.max_power", "limit.max_power",
), ),
PowerLimit: amdMetricAt(row, "W", "limit.ppt0.socket_power_limit", "limit.socket_power_limit"),
MemUsed: "N/A", MemUsed: "N/A",
MemTotal: "N/A", MemTotal: "N/A",
GPUUtil: "N/A", GPUUtil: "N/A",
@@ -150,11 +170,31 @@ func applyAMDMetrics(info *Info, data string) error {
continue continue
} }
setAMDMetric(&device.GPUUtil, row, "%", "usage.gfx_activity", "usage.gfx", "gfx_activity", "gfx_usage") setAMDMetric(&device.GPUUtil, row, "%", "usage.gfx_activity", "usage.gfx", "gfx_activity", "gfx_usage")
setAMDMetric(&device.Temperature, row, "°C", "temperature.hotspot", "temperature.edge", "hotspot_temperature", "gpu_temperature", "gpu_temp") setAMDMetric(&device.Temperature, row, "°C", "temperature.edge", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.PowerDraw, row, "W", "power.socket_power", "socket_power", "power_usage") setAMDMetric(&device.PowerDraw, row, "W", "power.socket_power", "socket_power", "power_usage")
setAMDMetric(&device.MemUsed, row, "MB", "mem_usage.used_vram", "vram.used", "used_vram", "vram_used") setAMDMetric(&device.MemUsed, row, "MB", "mem_usage.used_vram", "vram.used", "used_vram", "vram_used")
setAMDMetric(&device.MemTotal, row, "MB", "mem_usage.total_vram", "vram.total", "total_vram", "vram_total") setAMDMetric(&device.MemTotal, row, "MB", "mem_usage.total_vram", "vram.total", "total_vram", "vram_total")
setAMDMetric(&device.FanSpeed, row, "%", "fan.speed", "fan_speed")
setAMDMetric(&device.FanSpeed, row, "%", "fan.usage")
if amdMetricAt(row, "%", "fan.usage") == "" {
speed, speedErr := strconv.ParseFloat(amdStringAt(row, "fan.speed"), 64)
maximum, maxErr := strconv.ParseFloat(amdStringAt(row, "fan.max"), 64)
if speedErr == nil && maxErr == nil && !math.IsNaN(speed) && !math.IsInf(speed, 0) && maximum > 0 && !math.IsInf(maximum, 0) && speed >= 0 && speed <= maximum {
device.FanSpeed = fmt.Sprintf("%.2f %%", speed/maximum*100)
}
}
setAMDMetric(&device.FanRPM, row, "RPM", "fan.rpm")
setAMDMetric(&device.HotspotTemperature, row, "°C", "temperature.hotspot", "hotspot_temperature")
setAMDMetric(&device.MemoryTemperature, row, "°C", "temperature.mem")
setAMDMetric(&device.MemoryActivity, row, "%", "usage.umc_activity")
setAMDMetric(&device.MediaUtil, row, "%", "usage.mm_activity")
setAMDMetric(&device.Frequency, row, "MHz", "clock.gfx_0.clk")
setAMDMetric(&device.MemoryFrequency, row, "MHz", "clock.mem_0.clk")
correctable := amdStringAt(row, "ecc.total_correctable_count", "ecc.correctable_count")
uncorrectable := amdStringAt(row, "ecc.total_uncorrectable_count", "ecc.uncorrectable_count")
if correctable != "" || uncorrectable != "" {
device.ECCErrors = []ECCError{{Scope: "Total", Correctable: correctable, Uncorrectable: uncorrectable}}
}
if value := amdStringAt(row, "perf_level", "performance_level"); value != "" { if value := amdStringAt(row, "perf_level", "performance_level"); value != "" {
device.PerformanceState = value device.PerformanceState = value
} }
@@ -177,6 +217,7 @@ func applyAMDProcesses(info *Info, data string) error {
if !ok { if !ok {
continue continue
} }
device.ProcessStatus = "ok"
processList, _ := amdValueAt(row, "process_list") processList, _ := amdValueAt(row, "process_list")
items := amdObjectList(processList) items := amdObjectList(processList)
if len(items) == 0 { if len(items) == 0 {
+4
View File
@@ -64,6 +64,7 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if result.info == nil { if result.info == nil {
continue continue
} }
merged.Warnings = append(merged.Warnings, result.info.Warnings...)
if result.info.Type != "" { if result.info.Type != "" {
types = append(types, result.info.Type) types = append(types, result.info.Type)
} }
@@ -86,6 +87,9 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if len(merged.Devices) == 0 && len(errs) > 0 { if len(merged.Devices) == 0 && len(errs) > 0 {
return nil, fmt.Errorf("calling GPU monitoring tools failed: %w", errors.Join(errs...)) return nil, fmt.Errorf("calling GPU monitoring tools failed: %w", errors.Join(errs...))
} }
for _, err := range errs {
merged.Warnings = append(merged.Warnings, err.Error())
}
return merged, nil return merged, nil
} }
+76 -69
View File
@@ -1,16 +1,12 @@
package gpu package gpu
import ( import (
"bytes"
"context" "context"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io"
"strings" "strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
) )
@@ -24,41 +20,10 @@ func (n nvidiaSMI) LoadInfo(ctx context.Context) (*Info, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", nvidiaSMICommand, err) return nil, fmt.Errorf("calling %s failed: %w", nvidiaSMICommand, err)
} }
data := []byte(itemData) return parseNvidiaSMI([]byte(itemData))
version := "v11"
buf := bytes.NewBuffer(data)
decoder := xml.NewDecoder(buf)
for {
token, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("reading token failed: %w", err)
}
d, ok := token.(xml.Directive)
if !ok {
continue
}
directive := string(d)
if !strings.HasPrefix(directive, "DOCTYPE") {
continue
}
parts := strings.Split(directive, " ")
s := strings.Trim(parts[len(parts)-1], "\" ")
if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") {
version = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
} else {
global.LOG.Debugf("Cannot find schema version in %q", directive)
}
break
}
return parseNvidiaSMI(data, version)
} }
func parseNvidiaSMI(buf []byte, version string) (*Info, error) { func parseNvidiaSMI(buf []byte) (*Info, error) {
var ( var (
s nvidiaSMIResponse s nvidiaSMIResponse
info Info info Info
@@ -72,42 +37,84 @@ func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
info.DriverVersion = s.DriverVersion info.DriverVersion = s.DriverVersion
for i := range s.Gpu { for i := range s.Gpu {
gpuItem := Device{ gpuItem := Device{
Type: "nvidia", Type: "nvidia",
Index: uint(i), MemoryActivity: s.Gpu[i].Utilization.MemoryUtil,
ProductName: s.Gpu[i].ProductName, EncoderUtil: s.Gpu[i].Utilization.EncoderUtil,
PersistenceMode: s.Gpu[i].PersistenceMode, DecoderUtil: s.Gpu[i].Utilization.DecoderUtil,
BusID: s.Gpu[i].ID, JPEGUtil: s.Gpu[i].Utilization.JpegUtil,
DisplayActive: s.Gpu[i].DisplayActive, OFAUtil: s.Gpu[i].Utilization.OfaUtil,
ECC: s.Gpu[i].EccErrors.Volatile.DramUncorrectable, MediaFrequency: s.Gpu[i].Clocks.VideoClock,
FanSpeed: s.Gpu[i].FanSpeed, UUID: s.Gpu[i].UUID,
Temperature: s.Gpu[i].Temperature.GpuTemp, DriverVersion: s.DriverVersion,
PerformanceState: s.Gpu[i].PerformanceState, Architecture: s.Gpu[i].ProductArchitecture,
MemUsed: s.Gpu[i].FbMemoryUsage.Used, Frequency: s.Gpu[i].Clocks.GraphicsClock,
MemTotal: s.Gpu[i].FbMemoryUsage.Total, MemoryFrequency: s.Gpu[i].Clocks.MemClock,
GPUUtil: s.Gpu[i].Utilization.GpuUtil, MemoryTemperature: s.Gpu[i].Temperature.MemoryTemp,
ComputeMode: s.Gpu[i].ComputeMode, MemoryFree: s.Gpu[i].FbMemoryUsage.Free,
MigMode: s.Gpu[i].MigMode.CurrentMig, MemoryReserved: s.Gpu[i].FbMemoryUsage.Reserved,
} PCIeGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.DeviceCurrentLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.CurrentLinkGen),
if version == "v12" || version == "v13" { PCIeMaxGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxDeviceLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxLinkGen),
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw PCIeWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.CurrentLinkWidth,
if gpuItem.PowerDraw == "" { PCIeMaxWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.MaxLinkWidth,
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.InstantPowerDraw ProcessStatus: "unavailable",
} Index: uint(i),
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.CurrentPowerLimit ProductName: s.Gpu[i].ProductName,
} else { PersistenceMode: s.Gpu[i].PersistenceMode,
gpuItem.PowerDraw = s.Gpu[i].PowerReadings.PowerDraw BusID: s.Gpu[i].ID,
gpuItem.MaxPowerLimit = s.Gpu[i].PowerReadings.MaxPowerLimit DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccMode.CurrentEcc,
FanSpeed: s.Gpu[i].FanSpeed,
Temperature: s.Gpu[i].Temperature.GpuTemp,
PerformanceState: s.Gpu[i].PerformanceState,
MemUsed: s.Gpu[i].FbMemoryUsage.Used,
MemTotal: s.Gpu[i].FbMemoryUsage.Total,
GPUUtil: s.Gpu[i].Utilization.GpuUtil,
ComputeMode: s.Gpu[i].ComputeMode,
MigMode: s.Gpu[i].MigMode.CurrentMig,
} }
gpuItem.ECCPending = s.Gpu[i].EccMode.PendingEcc
gpuItem.ECCErrors = []ECCError{
{Scope: "Volatile Total", Correctable: s.Gpu[i].EccErrors.Volatile.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DoubleBit.Total},
{Scope: "Aggregate Total", Correctable: s.Gpu[i].EccErrors.Aggregate.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DoubleBit.Total},
for _, process := range s.Gpu[i].Processes.ProcessInfo { {Scope: "Volatile DRAM", Correctable: s.Gpu[i].EccErrors.Volatile.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DramUncorrectable},
gpuItem.Processes = append(gpuItem.Processes, Process{ {Scope: "Volatile SRAM", Correctable: s.Gpu[i].EccErrors.Volatile.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.SramUncorrectable},
PID: process.Pid, {Scope: "Aggregate DRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DramUncorrectable},
Type: process.Type, {Scope: "Aggregate SRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.SramUncorrectable},
ProcessName: process.ProcessName, }
UsedMemory: process.UsedMemory, gpuItem.PowerDraw = firstSMIValue(s.Gpu[i].GpuPowerReadings.PowerDraw, s.Gpu[i].GpuPowerReadings.InstantPowerDraw, s.Gpu[i].PowerReadings.PowerDraw)
}) gpuItem.PowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.CurrentPowerLimit, s.Gpu[i].PowerReadings.EnforcedPowerLimit, s.Gpu[i].PowerReadings.PowerLimit)
gpuItem.MaxPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.MaxPowerLimit, s.Gpu[i].PowerReadings.MaxPowerLimit)
gpuItem.DefaultPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.DefaultPowerLimit, s.Gpu[i].PowerReadings.DefaultPowerLimit)
for _, event := range append(s.Gpu[i].ClocksEventReasons.Reasons, s.Gpu[i].ClocksThrottleReasons.Reasons...) {
if strings.EqualFold(strings.TrimSpace(event.Value), "Active") {
gpuItem.ClockEvents = append(gpuItem.ClockEvents, strings.TrimPrefix(strings.TrimPrefix(event.XMLName.Local, "clocks_event_reason_"), "clocks_throttle_reason_"))
}
}
if s.Gpu[i].Processes != nil && strings.TrimSpace(s.Gpu[i].Processes.Text) == "" {
gpuItem.ProcessStatus = "ok"
}
if s.Gpu[i].Processes != nil {
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
}
} }
info.Devices = append(info.Devices, gpuItem) info.Devices = append(info.Devices, gpuItem)
} }
return &info, nil return &info, nil
} }
func firstSMIValue(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" && !strings.EqualFold(value, "N/A") && !strings.EqualFold(value, "Not Supported") {
return value
}
}
return ""
}
+30 -13
View File
@@ -1,9 +1,12 @@
package gpu package gpu
import "encoding/xml"
type nvidiaSMIResponse struct { type nvidiaSMIResponse struct {
AttachedGpus string `xml:"attached_gpus"` XMLName xml.Name `xml:"nvidia_smi_log"`
CudaVersion string `xml:"cuda_version"` AttachedGpus string `xml:"attached_gpus"`
DriverVersion string `xml:"driver_version"` CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
Gpu []struct { Gpu []struct {
ID string `xml:"id,attr"` ID string `xml:"id,attr"`
AccountedProcesses struct{} `xml:"accounted_processes"` AccountedProcesses struct{} `xml:"accounted_processes"`
@@ -37,16 +40,17 @@ type nvidiaSMIResponse struct {
VideoClock string `xml:"video_clock"` VideoClock string `xml:"video_clock"`
} `xml:"clocks"` } `xml:"clocks"`
ClocksEventReasons struct { ClocksEventReasons struct {
ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"` Reasons []struct {
ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"` XMLName xml.Name
ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"` Value string `xml:",chardata"`
ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"` } `xml:",any"`
ClocksEventReasonHwSlowdown string `xml:"clocks_event_reason_hw_slowdown"`
ClocksEventReasonHwThermalSlowdown string `xml:"clocks_event_reason_hw_thermal_slowdown"`
ClocksEventReasonSwPowerCap string `xml:"clocks_event_reason_sw_power_cap"`
ClocksEventReasonSwThermalSlowdown string `xml:"clocks_event_reason_sw_thermal_slowdown"`
ClocksEventReasonSyncBoost string `xml:"clocks_event_reason_sync_boost"`
} `xml:"clocks_event_reasons"` } `xml:"clocks_event_reasons"`
ClocksThrottleReasons struct {
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `xml:"clocks_throttle_reasons"`
ComputeMode string `xml:"compute_mode"` ComputeMode string `xml:"compute_mode"`
DefaultApplicationsClocks struct { DefaultApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"` GraphicsClock string `xml:"graphics_clock"`
@@ -63,12 +67,24 @@ type nvidiaSMIResponse struct {
} `xml:"driver_model"` } `xml:"driver_model"`
EccErrors struct { EccErrors struct {
Aggregate struct { Aggregate struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"` DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"` DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"` SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"` SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"aggregate"` } `xml:"aggregate"`
Volatile struct { Volatile struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"` DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"` DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"` SramCorrectable string `xml:"sram_correctable"`
@@ -226,7 +242,8 @@ type nvidiaSMIResponse struct {
MinPowerLimit string `xml:"min_power_limit"` MinPowerLimit string `xml:"min_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"` MaxPowerLimit string `xml:"max_power_limit"`
} `xml:"power_readings"` } `xml:"power_readings"`
Processes struct { Processes *struct {
Text string `xml:",chardata"`
ProcessInfo []struct { ProcessInfo []struct {
Pid string `xml:"pid"` Pid string `xml:"pid"`
Type string `xml:"type"` Type string `xml:"type"`
+37
View File
@@ -1,6 +1,8 @@
package gpu package gpu
type Info struct { type Info struct {
Warnings []string `json:"warnings"`
CudaVersion string `json:"cudaVersion"` CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"` DriverVersion string `json:"driverVersion"`
Type string `json:"type"` Type string `json:"type"`
@@ -8,7 +10,42 @@ type Info struct {
Devices []Device `json:"gpu"` Devices []Device `json:"gpu"`
} }
type ECCError struct {
Scope string `json:"scope"`
Correctable string `json:"correctable"`
Uncorrectable string `json:"uncorrectable"`
}
type Device struct { type Device struct {
ECCPending string `json:"eccPending"`
ECCErrors []ECCError `json:"eccErrors"`
MemoryActivity string `json:"memoryActivity"`
EncoderUtil string `json:"encoderUtil"`
DecoderUtil string `json:"decoderUtil"`
JPEGUtil string `json:"jpegUtil"`
OFAUtil string `json:"ofaUtil"`
MediaUtil string `json:"mediaUtil"`
HotspotTemperature string `json:"hotspotTemperature"`
FanRPM string `json:"fanRPM"`
MediaFrequency string `json:"mediaFrequency"`
UUID string `json:"uuid"`
DriverVersion string `json:"driverVersion"`
Architecture string `json:"architecture"`
Frequency string `json:"frequency"`
MemoryFrequency string `json:"memoryFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryFree string `json:"memoryFree"`
MemoryReserved string `json:"memoryReserved"`
PowerLimit string `json:"powerLimit"`
DefaultPowerLimit string `json:"defaultPowerLimit"`
PCIeGeneration string `json:"pcieGeneration"`
PCIeMaxGeneration string `json:"pcieMaxGeneration"`
PCIeWidth string `json:"pcieWidth"`
PCIeMaxWidth string `json:"pcieMaxWidth"`
ProcessStatus string `json:"processStatus"`
ClockEvents []string `json:"clockEvents"`
Type string `json:"type"` Type string `json:"type"`
Index uint `json:"index"` Index uint `json:"index"`
ProductName string `json:"productName"` ProductName string `json:"productName"`
+61 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -34,7 +35,31 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", ascendSMICommand, err) return nil, fmt.Errorf("calling %s failed: %w", ascendSMICommand, err)
} }
return parseAscendSMI(itemData), nil
info := parseAscendSMI(itemData)
var wg sync.WaitGroup
warnings := make([]string, len(info.Devices))
for i := range info.Devices {
wg.Add(1)
go func(index int) {
defer wg.Done()
device := &info.Devices[index]
mgr := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second))
data, err := mgr.RunWithStdout(ascendSMICommand, "info", "-t", "usages", "-i", strconv.FormatUint(uint64(device.NPUIndex), 10), "-c", strconv.FormatUint(uint64(device.ChipIndex), 10))
if err != nil {
warnings[index] = fmt.Sprintf("npu-smi usages %d/%d: %v", device.NPUIndex, device.ChipIndex, err)
return
}
applyAscendUsages(device, data)
}(i)
}
wg.Wait()
for _, warning := range warnings {
if warning != "" {
info.Warnings = append(info.Warnings, warning)
}
}
return info, nil
} }
func parseAscendSMI(data string) *Info { func parseAscendSMI(data string) *Info {
@@ -158,6 +183,12 @@ func parseAscendSMI(data string) *Info {
pending = nil pending = nil
} }
for i := range info.Devices {
info.Devices[i].ProcessStatus = "unavailable"
if processSection {
info.Devices[i].ProcessStatus = "ok"
}
}
return info return info
} }
@@ -208,6 +239,9 @@ func ascendMemoryPools(value, header string) (string, string, string, string) {
} }
hbm := usage[len(usage)-1] hbm := usage[len(usage)-1]
if !strings.Contains(normalizedHeader, "MEMORYUSAGE") {
memoryUsed, memoryTotal = "", ""
}
return memoryUsed, memoryTotal, hbm[0] + " MB", hbm[1] + " MB" return memoryUsed, memoryTotal, hbm[0] + " MB", hbm[1] + " MB"
} }
@@ -233,3 +267,29 @@ func ascendValueWithUnit(value, unit string) string {
} }
return value + " " + unit return value + " " + unit
} }
func applyAscendUsages(device *Device, data string) {
for _, line := range strings.Split(data, "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.ToLower(strings.Join(strings.Fields(key), ""))
value = strings.TrimSpace(value)
switch key {
case "aicoreusagerate(%)":
usage, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(value, "%")), 64)
if err == nil && usage >= 0 && usage <= 100 {
device.AICore = ascendValueWithUnit(value, "%")
}
case "aicpuusagerate(%)":
device.AICPUUtil = ascendValueWithUnit(value, "%")
case "ctrlcpuusagerate(%)":
device.CtrlCPUUtil = ascendValueWithUnit(value, "%")
case "ddrbandwidthusagerate(%)", "memorybandwidthusagerate(%)":
device.DDRBandwidth = ascendValueWithUnit(value, "%")
case "hbmbandwidthusagerate(%)":
device.HBMBandwidth = ascendValueWithUnit(value, "%")
}
}
}
+8
View File
@@ -1,12 +1,20 @@
package npu package npu
type Info struct { type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"` Type string `json:"type"`
DriverVersion string `json:"driverVersion"` DriverVersion string `json:"driverVersion"`
Devices []Device `json:"npu"` Devices []Device `json:"npu"`
} }
type Device struct { type Device struct {
AICPUUtil string `json:"aiCPUUtil"`
CtrlCPUUtil string `json:"ctrlCPUUtil"`
DDRBandwidth string `json:"ddrBandwidth"`
HBMBandwidth string `json:"hbmBandwidth"`
ProcessStatus string `json:"processStatus"`
Type string `json:"type"` Type string `json:"type"`
Index uint `json:"index"` Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"` NPUIndex uint `json:"npuIndex"`
+28 -2
View File
@@ -33,16 +33,29 @@ type discoveryInfo struct {
} }
type DeviceLevelMetric struct { type DeviceLevelMetric struct {
MetricsType string `json:"metrics_type"` MetricsType string `json:"metrics_type"`
Value float64 `json:"value"` Value *float64 `json:"value"`
}
type TileMetrics struct {
TileID int `json:"tile_id"`
DataList []DeviceLevelMetric `json:"data_list"`
}
type TileStats struct {
TileID int `json:"tileID"`
Stats Stats `json:"stats"`
} }
type DeviceStats struct { type DeviceStats struct {
TileLevel []TileMetrics `json:"tile_level"`
DeviceID int `json:"device_id"` DeviceID int `json:"device_id"`
DeviceLevel []DeviceLevelMetric `json:"device_level"` DeviceLevel []DeviceLevelMetric `json:"device_level"`
} }
type Info struct { type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"` Type string `json:"type"`
DriverVersion string `json:"driverVersion"` DriverVersion string `json:"driverVersion"`
@@ -50,12 +63,17 @@ type Info struct {
} }
type Device struct { type Device struct {
Tiles []TileStats `json:"tiles"`
ProcessStatus string `json:"processStatus"`
Basic Basic `json:"basic"` Basic Basic `json:"basic"`
Stats Stats `json:"stats"` Stats Stats `json:"stats"`
Processes []Process `json:"processes"` Processes []Process `json:"processes"`
} }
type Basic struct { type Basic struct {
UUID string `json:"uuid"`
DeviceID int `json:"deviceID"` DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"` DeviceName string `json:"deviceName"`
VendorName string `json:"vendorName"` VendorName string `json:"vendorName"`
@@ -66,6 +84,14 @@ type Basic struct {
} }
type Stats struct { type Stats struct {
MediaUtil string `json:"mediaUtil"`
ComputeUtil string `json:"computeUtil"`
CopyUtil string `json:"copyUtil"`
MediaFrequency string `json:"mediaFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryBandwidthUtil string `json:"memoryBandwidthUtil"`
Power string `json:"power"` Power string `json:"power"`
GPUUtil string `json:"gpuUtil"` GPUUtil string `json:"gpuUtil"`
Frequency string `json:"frequency"` Frequency string `json:"frequency"`
+53 -13
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"sort" "sort"
"strconv" "strconv"
"sync" "sync"
@@ -51,12 +52,15 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
processData, err := cmdMgr.RunWithStdout(xpuSMICommand, "ps", "-j") processData, err := cmdMgr.RunWithStdout(xpuSMICommand, "ps", "-j")
if err != nil { if err != nil {
global.LOG.Warnf("calling xpu-smi ps failed, process information will be omitted: %v", err) res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps: %v", err))
} else { } else {
var psList DeviceUtilByProcList var psList DeviceUtilByProcList
if err := json.Unmarshal([]byte(processData), &psList); err != nil { if err := json.Unmarshal([]byte(processData), &psList); err != nil {
global.LOG.Warnf("processData json unmarshal failed, process information will be omitted: %v", err) res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps output: %v", err))
} else { } else {
for i := range res.Devices {
res.Devices[i].ProcessStatus = "ok"
}
for _, ps := range psList.DeviceUtilByProcList { for _, ps := range psList.DeviceUtilByProcList {
process := Process{ process := Process{
PID: ps.ProcessID, PID: ps.ProcessID,
@@ -87,7 +91,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
defer wg.Done() defer wg.Done()
xpu := Device{ xpu := Device{
ProcessStatus: "unavailable",
Basic: Basic{ Basic: Basic{
UUID: device.UUID,
DeviceID: device.DeviceID, DeviceID: device.DeviceID,
DeviceName: device.DeviceName, DeviceName: device.DeviceName,
VendorName: device.VendorName, VendorName: device.VendorName,
@@ -115,13 +121,19 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
wgCmd.Wait() wgCmd.Wait()
if xpuErr != nil { if xpuErr != nil {
global.LOG.Errorf("calling xpu-smi discovery failed for device %d, %v", device.DeviceID, xpuErr) mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d: %v", device.DeviceID, xpuErr))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return return
} }
var info discoveryDevice var info discoveryDevice
if err := json.Unmarshal([]byte(xpuData), &info); err != nil { if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
global.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v", device.DeviceID, err) mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d output: %v", device.DeviceID, err))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return return
} }
@@ -142,13 +154,22 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
} }
if statsErr != nil { if statsErr != nil {
global.LOG.Warnf("calling xpu-smi stats failed for device %d, metrics will be omitted: %v", device.DeviceID, statsErr) mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d: %v", device.DeviceID, statsErr))
mu.Unlock()
} else { } else {
var stats DeviceStats var stats DeviceStats
if err := json.Unmarshal([]byte(statsData), &stats); err != nil { if err := json.Unmarshal([]byte(statsData), &stats); err != nil {
global.LOG.Warnf("statsData json unmarshal failed for device %d, metrics will be omitted: %v", device.DeviceID, err) mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d output: %v", device.DeviceID, err))
mu.Unlock()
} else { } else {
loadStats(&xpu.Stats, stats.DeviceLevel) loadStats(&xpu.Stats, stats.DeviceLevel)
for _, tile := range stats.TileLevel {
item := TileStats{TileID: tile.TileID}
loadStats(&item.Stats, tile.DataList)
xpu.Tiles = append(xpu.Tiles, item)
}
} }
} }
@@ -162,19 +183,38 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
func loadStats(stats *Stats, metrics []DeviceLevelMetric) { func loadStats(stats *Stats, metrics []DeviceLevelMetric) {
for _, stat := range metrics { for _, stat := range metrics {
if stat.Value == nil || math.IsNaN(*stat.Value) || math.IsInf(*stat.Value, 0) {
continue
}
value := *stat.Value
if value < 0 && stat.MetricsType != "XPUM_STATS_GPU_CORE_TEMPERATURE" && stat.MetricsType != "XPUM_STATS_MEMORY_TEMPERATURE" {
continue
}
switch stat.MetricsType { switch stat.MetricsType {
case "XPUM_STATS_MEMORY_TEMPERATURE":
stats.MemoryTemperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_ENGINE_GROUP_COMPUTE_ALL_UTILIZATION":
stats.ComputeUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_MEDIA_ALL_UTILIZATION":
stats.MediaUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_COPY_ALL_UTILIZATION":
stats.CopyUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEDIA_ENGINE_FREQUENCY":
stats.MediaFrequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_POWER": case "XPUM_STATS_POWER":
stats.Power = fmt.Sprintf("%.1fW", stat.Value) stats.Power = fmt.Sprintf("%.1fW", value)
case "XPUM_STATS_GPU_UTILIZATION": case "XPUM_STATS_GPU_UTILIZATION":
stats.GPUUtil = fmt.Sprintf("%.1f%%", stat.Value) stats.GPUUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_GPU_FREQUENCY": case "XPUM_STATS_GPU_FREQUENCY":
stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value) stats.Frequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_GPU_CORE_TEMPERATURE": case "XPUM_STATS_GPU_CORE_TEMPERATURE":
stats.Temperature = fmt.Sprintf("%.1f°C", stat.Value) stats.Temperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_MEMORY_USED": case "XPUM_STATS_MEMORY_USED":
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", stat.Value) stats.MemoryUsed = fmt.Sprintf("%.1f MiB", value)
case "XPUM_STATS_MEMORY_UTILIZATION", "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION": case "XPUM_STATS_MEMORY_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value) stats.MemoryUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryBandwidthUtil = fmt.Sprintf("%.1f%%", value)
} }
} }
} }
+48 -7
View File
@@ -24,10 +24,10 @@ import (
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
) )
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"} var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp", "syncIpGroup", "cleanLog"}
func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error { func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) params := CreateTaskAlertParams(pushAlert)
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method) alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert) alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule create.AlertRule = alertRule
@@ -76,14 +76,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
Encryption: emailInfo.Encryption, Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient, Recipient: emailInfo.Recipient,
} }
content := GetSendContent(alert.Type, params, agentInfo) content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" { if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title}) content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
} }
msg := email.EmailMessage{ msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("PanelAlertTitle"), Subject: i18n.GetMsgByKey("PanelAlertTitle"),
Body: content, Body: content,
IsHTML: true, IsHTML: GetCronJobType(alert.Type) != "cronJob",
} }
if err = email.SendMail(smtpConfig, msg, transport); err != nil { if err = email.SendMail(smtpConfig, msg, transport); err != nil {
@@ -110,7 +110,7 @@ func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []
return SaveAlertLog(create, &alertLog) return SaveAlertLog(create, &alertLog)
} }
content := GetSendContent(alert.Type, params, agentInfo) content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" { if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title}) content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
} }
@@ -268,7 +268,7 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
alertDetail := dto.AlertDetail{ alertDetail := dto.AlertDetail{
Type: GetCronJobType(alert.Type), Type: GetCronJobType(alert.Type),
SubType: alert.Type, SubType: alert.Type,
Title: alert.Title, Title: TaskAlertTitle(alert.Type, alert.Title, project, params),
Method: method, Method: method,
Project: project, Project: project,
Params: params, Params: params,
@@ -281,6 +281,14 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
return string(marshal) return string(marshal)
} }
func TaskAlertTitle(alertType, title, project string, params []dto.Param) string {
if GetCronJobType(alertType) != "cronJob" || CronJobAlertResultFromParams(params) != CronJobAlertSuccess {
return title
}
name := cronJobTaskName(project, params)
return i18n.GetMsgWithMap("TaskSuccess", map[string]interface{}{"name": name})
}
func ProcessAlertRule(alert dto.AlertDTO) string { func ProcessAlertRule(alert dto.AlertDTO) string {
marshal, err := json.Marshal(alert) marshal, err := json.Marshal(alert)
if err != nil { if err != nil {
@@ -324,6 +332,10 @@ func GetCronJobTypeName(cronJobType string) string {
module = "系统快照" module = "系统快照"
case "ntp": case "ntp":
module = "同步服务器时间" module = "同步服务器时间"
case "syncIpGroup":
module = "同步 IP 组"
case "cleanLog":
module = "清理日志"
default: default:
} }
return module return module
@@ -444,6 +456,30 @@ func isWithinTimeRange(savedTimeString string) bool {
} }
func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentInfo) string { func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentInfo) string {
return GetAlertDetailContent(dto.AlertDetail{Type: alertType, Params: params}, agentInfo)
}
func GetAlertLogContent(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, agentInfo *dto.AgentInfo) string {
detail := dto.AlertDetail{Type: alert.Type, Params: params}
var stored dto.AlertDetail
if json.Unmarshal([]byte(create.AlertDetail), &stored) == nil {
detail = stored
if detail.Type == "" {
detail.Type = alert.Type
}
if detail.Params == nil {
detail.Params = params
}
}
return GetAlertDetailContent(detail, agentInfo)
}
func GetAlertDetailContent(detail dto.AlertDetail, agentInfo *dto.AgentInfo) string {
alertType := detail.SubType
if alertType == "" {
alertType = detail.Type
}
params := detail.Params
switch GetCronJobType(alertType) { switch GetCronJobType(alertType) {
case "ssl": case "ssl":
return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
@@ -464,7 +500,12 @@ func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentIn
case "disk": case "disk":
return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "cronJob": case "cronJob":
return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) messageKey := "CronJobFailedAlert"
if CronJobAlertResultFromParams(params) == CronJobAlertSuccess {
messageKey = "CronJobSuccessAlert"
}
name := cronJobTaskName(detail.Project, params)
return i18n.GetMsgWithMap(messageKey, map[string]interface{}{"name": name, "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "clams": case "clams":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "panelLogin": case "panelLogin":
+130
View File
@@ -0,0 +1,130 @@
package alert
import (
"encoding/json"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
const (
CronJobAlertFailed = "failed"
CronJobAlertSuccess = "success"
CronJobAlertBoth = "both"
)
func cronJobAlertParams(advanced string) (map[string]json.RawMessage, error) {
params := make(map[string]json.RawMessage)
if strings.TrimSpace(advanced) != "" {
if err := json.Unmarshal([]byte(advanced), &params); err != nil {
return nil, fmt.Errorf("invalid cronjob alert advanced parameters: %w", err)
}
}
if params == nil {
params = make(map[string]json.RawMessage)
}
return params, nil
}
func CronJobAlertTriggerMode(advanced string) (string, error) {
params, err := cronJobAlertParams(advanced)
if err != nil {
return CronJobAlertFailed, err
}
mode := CronJobAlertFailed
if raw, ok := params["alertTriggerMode"]; ok {
if err := json.Unmarshal(raw, &mode); err != nil {
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode: %w", err)
}
}
switch mode {
case CronJobAlertFailed, CronJobAlertSuccess, CronJobAlertBoth:
return mode, nil
default:
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode %q", mode)
}
}
func MergeCronJobAlertParams(previous, incoming string) (string, error) {
params, err := cronJobAlertParams(previous)
if err != nil {
return "", err
}
updates, err := cronJobAlertParams(incoming)
if err != nil {
return "", err
}
for key, value := range updates {
params[key] = value
}
if _, ok := params["alertTriggerMode"]; !ok {
params["alertTriggerMode"] = json.RawMessage(`"failed"`)
}
data, err := json.Marshal(params)
if err != nil {
return "", err
}
if _, err := CronJobAlertTriggerMode(string(data)); err != nil {
return "", err
}
return string(data), nil
}
func MatchCronJobAlertResult(advanced, result string) bool {
mode, err := CronJobAlertTriggerMode(advanced)
if err != nil {
return false
}
if result == "" {
result = CronJobAlertFailed
}
if result != CronJobAlertFailed && result != CronJobAlertSuccess {
return false
}
return mode == CronJobAlertBoth || mode == result
}
func CreateTaskAlertParams(pushAlert dto.PushAlert) []dto.Param {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
if GetCronJobType(pushAlert.AlertType) != "cronJob" {
return params
}
params = append(params, CreateCronJobResultParam(pushAlert.Result))
return params
}
func cronJobTaskName(project string, params []dto.Param) string {
if project != "" {
return project
}
if name := getValueByIndex(params, "taskName"); name != "" {
return name
}
return getValueByIndex(params, "1")
}
func CreateCronJobResultParam(result string) dto.Param {
value := "失败"
if result == CronJobAlertSuccess {
value = "成功"
}
return dto.Param{Index: "2", Key: "result", Value: value}
}
func CronJobAlertResultFromParams(params []dto.Param) string {
for _, param := range params {
if param.Index == "result" {
if param.Value == CronJobAlertSuccess {
return CronJobAlertSuccess
}
return CronJobAlertFailed
}
}
for _, param := range params {
if param.Index == "2" && param.Key == "result" && param.Value == "成功" {
return CronJobAlertSuccess
}
}
return CronJobAlertFailed
}
+2 -2
View File
@@ -46,7 +46,7 @@ func CreateTaskScanCustomWebhookAlertLog(
transport *http.Transport, transport *http.Transport,
agentInfo *dto.AgentInfo, agentInfo *dto.AgentInfo,
) error { ) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) params := CreateTaskAlertParams(pushAlert)
alertInfo := info alertInfo := info
alertInfo.Type = alertType alertInfo.Type = alertType
create.Type = GetCronJobType(alertType) create.Type = GetCronJobType(alertType)
@@ -99,7 +99,7 @@ func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occur
if businessType == "" { if businessType == "" {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed") return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
} }
content := GetSendContent(businessType, detail.Params, agentInfo) content := GetAlertDetailContent(detail, agentInfo)
if content == "" { if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title}) content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title})
} }
+4 -1
View File
@@ -26,6 +26,9 @@ func PushAlert(pushAlert dto.PushAlert) error {
} }
var alert dto.AlertDTO var alert dto.AlertDTO
_ = copier.Copy(&alert, &alertInfo) _ = copier.Copy(&alert, &alertInfo)
if alertUtil.GetCronJobType(pushAlert.AlertType) == "cronJob" && !alertUtil.MatchCronJobAlertResult(alert.AdvancedParams, pushAlert.Result) {
return nil
}
methods := strings.Split(alert.Method, ",") methods := strings.Split(alert.Method, ",")
for _, m := range methods { for _, m := range methods {
@@ -126,7 +129,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) params := alertUtil.CreateTaskAlertParams(pushAlert)
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark) alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert) alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule create.AlertRule = alertRule
+9
View File
@@ -27,6 +27,7 @@ type CommandHelper struct {
outputFile string outputFile string
scriptPath string scriptPath string
stdin io.Reader stdin io.Reader
stderr io.Writer
env []string env []string
timeout time.Duration timeout time.Duration
taskItem *task.Task taskItem *task.Task
@@ -360,6 +361,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
} }
if c.stderr != nil {
cmd.Stderr = io.MultiWriter(cmd.Stderr, c.stderr)
}
env := os.Environ() env := os.Environ()
env = append(env, c.env...) env = append(env, c.env...)
cmd.Env = env cmd.Env = env
@@ -481,6 +485,11 @@ func WithStdin(stdin io.Reader) Option {
s.stdin = stdin s.stdin = stdin
} }
} }
func WithStderr(stderr io.Writer) Option {
return func(s *CommandHelper) {
s.stderr = stderr
}
}
func WithEnv(env ...string) Option { func WithEnv(env ...string) Option {
return func(s *CommandHelper) { return func(s *CommandHelper) {
s.env = append(s.env, env...) s.env = append(s.env, env...)
+108
View File
@@ -0,0 +1,108 @@
package docker
import (
"context"
"sort"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type NetworkCleanupClient interface {
NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error)
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
NetworkInspect(context.Context, string, network.InspectOptions) (network.Inspect, error)
NetworkRemove(context.Context, string) error
}
func CleanUnusedNetworks(ctx context.Context, cli NetworkCleanupClient, onResult ...func(string, dto.NetworkCleanupItem)) (*dto.NetworkCleanupReport, error) {
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, 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
}
}
}
report := &dto.NetworkCleanupReport{Deleted: []dto.NetworkCleanupItem{}, Skipped: []dto.NetworkCleanupItem{}, Failed: []dto.NetworkCleanupItem{}}
record := func(status string, item dto.NetworkCleanupItem) {
switch status {
case "deleted":
report.Deleted = append(report.Deleted, item)
case "skipped":
report.Skipped = append(report.Skipped, item)
case "failed":
report.Failed = append(report.Failed, item)
}
for _, notify := range onResult {
if notify != nil {
notify(status, item)
}
}
}
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 report, err
}
item := dto.NetworkCleanupItem{ID: n.ID, Name: n.Name}
switch {
case n.Name == "none" || n.Name == "host" || n.Name == "bridge" || n.Name == "1panel-network":
item.Reason = "protected"
case n.Scope != "local" || n.Ingress || n.ConfigOnly:
item.Reason = "unsupported_network"
case used[n.Name] || used[n.ID]:
item.Reason = "container_connected"
}
if item.Reason != "" {
record("skipped", item)
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if errdefs.IsNotFound(err) {
item.Reason = "already_removed"
record("skipped", item)
} else {
item.Reason = "inspect_failed"
record("failed", item)
}
continue
}
if len(inspected.Containers) > 0 {
item.Reason = "container_connected"
record("skipped", item)
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
switch {
case errdefs.IsNotFound(err):
item.Reason = "already_removed"
record("skipped", item)
case errdefs.IsConflict(err):
item.Reason = "network_in_use"
record("skipped", item)
default:
item.Reason = "remove_failed"
record("failed", item)
}
continue
}
record("deleted", item)
}
return report, nil
}
@@ -0,0 +1,72 @@
package docker
import (
"context"
"errors"
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type cleanupFake struct {
networks []network.Inspect
containers []container.Summary
inspected map[string]network.Inspect
inspectErrors, removeErrors map[string]error
listError error
removed []string
}
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return f.networks, nil
}
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
if !o.All {
panic("must include stopped containers")
}
return f.containers, f.listError
}
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
return f.inspected[id], f.inspectErrors[id]
}
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
f.removed = append(f.removed, id)
return f.removeErrors[id]
}
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
}
// Compose ownership labels do not protect an otherwise unused network.
for i := range f.networks {
if f.networks[i].Name == "configured" {
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
}
}
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
report, err := CleanUnusedNetworks(context.Background(), f)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
t.Fatal(f.removed)
}
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
t.Fatalf("%+v", report)
}
}
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
t.Fatal("expected error")
}
if len(f.removed) > 0 {
t.Fatal(f.removed)
}
}
@@ -0,0 +1,247 @@
package docker_guard
import (
"net/netip"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func ReadDNATRules(backend, family string) DNATRules {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
if backend == constant.FirewallProviderNftables {
tableFamily := "ip"
if family == constant.FirewallFamilyIPv6 {
tableFamily = "ip6"
}
tables, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "tables")
if err != nil {
return DNATRules{}
}
if !strings.Contains(tables, "table "+tableFamily+" docker-bridges") {
return DNATRules{Inspected: true}
}
output, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "table", tableFamily, "docker-bridges")
return DNATRules{Output: output, Inspected: err == nil}
}
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return DNATRules{}
}
executable := commands.IPv4
if family == constant.FirewallFamilyIPv6 {
executable = commands.IPv6
}
if executable == "" {
return DNATRules{}
}
output, err := manager.RunWithOptionalSudoAndStdout(executable, "-w", "-t", "nat", "-S")
return DNATRules{Output: output, Inspected: err == nil}
}
func ReadProxyEndpoints() ProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil {
return ProxyEndpoints{}
}
return ProxyEndpoints{Items: parseDockerProxyEndpoints(output), Inspected: true}
}
func parseDockerProxyEndpoints(output string) []ProxyEndpoint {
result := make([]ProxyEndpoint, 0)
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
isProxy := slices.ContainsFunc(fields, func(field string) bool { return filepath.Base(field) == "docker-proxy" })
if !isProxy {
continue
}
protocol := commandFlagValue(fields, "-proto")
hostIP := commandFlagValue(fields, "-host-ip")
hostPortValue := commandFlagValue(fields, "-host-port")
hostPort, err := strconv.ParseUint(hostPortValue, 10, 16)
if err != nil || (protocol != "tcp" && protocol != "udp") || hostIP == "" {
continue
}
hostIP = strings.TrimSpace(hostIP)
if address, err := netip.ParseAddr(hostIP); err == nil {
hostIP = address.String()
}
result = append(result, ProxyEndpoint{Protocol: protocol, HostIP: hostIP, HostPort: uint16(hostPort)})
}
return result
}
func commandFlagValue(fields []string, name string) string {
for i := 0; i < len(fields); i++ {
if fields[i] == name && i+1 < len(fields) {
return fields[i+1]
}
if strings.HasPrefix(fields[i], name+"=") {
return strings.TrimPrefix(fields[i], name+"=")
}
}
return ""
}
func ProxyEndpointMatches(proxies []ProxyEndpoint, family, hostIP string, hostPort uint16, protocol string) bool {
for _, proxy := range proxies {
if proxy.Protocol == protocol && proxy.HostPort == hostPort && hostAddressMatches(proxy.HostIP, hostIP, family) {
return true
}
}
return false
}
func DNATRuleMatches(backend, output string, family, hostIP string, hostPort uint16, protocol string) bool {
return InspectEndpoints(backend, family, DNATRules{Output: output}, ProxyEndpoints{}).DNATMatches(hostIP, hostPort, protocol)
}
func DNATIngressReachable(backend, output string) bool {
if backend == constant.FirewallProviderNftables {
return strings.Contains(output, "hook prerouting")
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == "-A" && fields[1] == "PREROUTING" && commandFlagValue(fields, "-j") == "DOCKER" {
return true
}
}
return false
}
type EndpointInspection struct {
DNATInspected, ProxyInspected, IngressReachable bool
family string
dnat, proxies map[ProxyEndpoint]bool
}
func InspectEndpoints(backend, family string, rules DNATRules, proxies ProxyEndpoints) EndpointInspection {
inspection := EndpointInspection{
DNATInspected: rules.Inspected, ProxyInspected: proxies.Inspected,
IngressReachable: DNATIngressReachable(backend, rules.Output), family: family,
dnat: make(map[ProxyEndpoint]bool), proxies: make(map[ProxyEndpoint]bool),
}
for _, proxy := range proxies.Items {
if isWildcardHostAddress(proxy.HostIP, family) {
wildcard := proxy
wildcard.HostIP = ""
inspection.proxies[wildcard] = true
}
proxy.HostIP = normalizedEndpointAddress(proxy.HostIP)
inspection.proxies[proxy] = true
}
replacer := strings.NewReplacer("{", " ", "}", " ", ",", " ", ";", " ")
addressToken := "ip"
if family == constant.FirewallFamilyIPv6 {
addressToken = "ip6"
}
for line := range strings.SplitSeq(rules.Output, "\n") {
if backend != constant.FirewallProviderNftables {
fields := strings.Fields(line)
if commandFlagValue(fields, "-j") != "DNAT" {
continue
}
port, err := strconv.ParseUint(commandFlagValue(fields, "--dport"), 10, 16)
if err != nil || strconv.FormatUint(port, 10) != commandFlagValue(fields, "--dport") {
continue
}
address, _, _ := strings.Cut(commandFlagValue(fields, "-d"), "/")
inspection.dnat[ProxyEndpoint{Protocol: commandFlagValue(fields, "-p"), HostIP: normalizedEndpointAddress(address), HostPort: uint16(port)}] = true
continue
}
fields := strings.Fields(replacer.Replace(line))
if !slices.Contains(fields, "dnat") {
continue
}
destination := ""
protocols := make([]string, 0, 1)
for i := 0; i+2 < len(fields); i++ {
if fields[i] == "meta" && fields[i+1] == "l4proto" {
protocols = append(protocols, fields[i+2])
}
if destination == "" && fields[i] == addressToken && fields[i+1] == "daddr" {
destination = fields[i+2]
}
}
destination, _, _ = strings.Cut(destination, "/")
destination = normalizedEndpointAddress(destination)
for i := 0; i+2 < len(fields); i++ {
if fields[i+1] != "dport" {
continue
}
port, err := strconv.ParseUint(fields[i+2], 10, 16)
if err != nil || strconv.FormatUint(port, 10) != fields[i+2] {
continue
}
matches := []string{fields[i]}
if fields[i] == "th" {
matches = protocols
}
for _, protocol := range matches {
inspection.dnat[ProxyEndpoint{Protocol: protocol, HostIP: destination, HostPort: uint16(port)}] = true
}
}
}
return inspection
}
func normalizedEndpointAddress(address string) string {
address = strings.TrimSpace(address)
if parsed, err := netip.ParseAddr(address); err == nil {
return parsed.String()
}
return address
}
func (inspection EndpointInspection) DNATMatches(hostIP string, hostPort uint16, protocol string) bool {
key := ProxyEndpoint{Protocol: protocol, HostPort: hostPort}
if inspection.dnat[key] {
return true
}
if isWildcardHostAddress(hostIP, inspection.family) {
return false
}
key.HostIP = normalizedEndpointAddress(hostIP)
return inspection.dnat[key]
}
func (inspection EndpointInspection) ProxyMatches(hostIP string, hostPort uint16, protocol string) bool {
key := ProxyEndpoint{Protocol: protocol, HostPort: hostPort, HostIP: normalizedEndpointAddress(hostIP)}
if inspection.proxies[key] {
return true
}
if !isWildcardHostAddress(hostIP, inspection.family) {
return false
}
key.HostIP = ""
return inspection.proxies[key]
}
func hostAddressMatches(left, right, family string) bool {
if isWildcardHostAddress(left, family) && isWildcardHostAddress(right, family) {
return true
}
left, right = strings.TrimSpace(left), strings.TrimSpace(right)
if address, err := netip.ParseAddr(left); err == nil {
left = address.String()
}
if address, err := netip.ParseAddr(right); err == nil {
right = address.String()
}
return left == right
}
func isWildcardHostAddress(value, family string) bool {
value = strings.TrimSpace(value)
if family == constant.FirewallFamilyIPv6 {
return value == "" || value == "::"
}
return value == "" || value == "0.0.0.0"
}
+23 -90
View File
@@ -3,73 +3,19 @@ package docker_guard
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"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" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
) )
const (
Chain = "1PANEL_DOCKER"
DockerChain = "DOCKER-USER"
FamilyIPv4 = constant.FirewallFamilyIPv4
FamilyIPv6 = constant.FirewallFamilyIPv6
ModeSources = "deny_sources"
ModeAllow = "allow_sources"
ModeAll = "deny_all"
StatusEffective = "effective"
StatusDisabled = "disabled"
StatusNotEffective = "not_effective"
ReasonCommandMissing = "command_missing"
ReasonDockerChainMissing = "docker_chain_missing"
ReasonGuardChainMissing = "guard_chain_missing"
ReasonJumpMissing = "jump_missing"
ReasonJumpNotFirst = "jump_not_first"
ReasonJumpDuplicate = "jump_duplicate"
ReasonInspectFailed = "inspect_failed"
)
var (
ErrDockerChainUnavailable = errors.New("Docker DOCKER-USER chain is unavailable")
ErrDockerIptablesChainUnavailable = fmt.Errorf("%w for iptables", ErrDockerChainUnavailable)
ErrDockerNftablesChainUnavailable = fmt.Errorf("%w for nftables", ErrDockerChainUnavailable)
)
type FamilyError struct {
Family string
Err error
}
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
func (e *FamilyError) Unwrap() error { return e.Err }
type Policy struct {
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
}
type FamilyStatus struct {
State string
Reason string
Initialized bool
Bound bool
Effective bool
}
type Runner interface { type Runner interface {
Run(executable string, args ...string) (string, error) Run(executable string, args ...string) (string, error)
RunInput(executable, input string, args ...string) (string, error) RunInput(executable, input string, args ...string) (string, error)
@@ -122,24 +68,20 @@ func dockerGuardExecutable(logical string) string {
} }
} }
type Manager struct { type Iptables struct {
runner Runner runner Runner
} }
var mutationMu sync.Mutex var mutationMu sync.Mutex
func NewManager() *Manager { return &Manager{runner: commandRunner{}} } func NewIptables() *Iptables { return &Iptables{runner: commandRunner{}} }
func (m *Manager) Initialize(policies []Policy) error { func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := CheckIPv4Forwarding(); err != nil { if err := CheckIPv4Forwarding(); err != nil {
return err return err
} }
inventory, err := m.ListPolicies()
if err != nil {
return err
}
if !m.runner.Exists("iptables-restore") { if !m.runner.Exists("iptables-restore") {
return errors.New("iptables-restore is not installed") return errors.New("iptables-restore is not installed")
} }
@@ -163,7 +105,7 @@ func (m *Manager) Initialize(policies []Policy) error {
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *Manager) Bind() error { func (m *Iptables) Bind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily("iptables", true); err != nil { if err := m.bindExistingFamily("iptables", true); err != nil {
@@ -177,17 +119,13 @@ func (m *Manager) Bind() error {
return nil return nil
} }
func (m *Manager) Reconcile(policies []Policy) error { func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *Manager) ListPolicies() (PolicyInventory, error) { func (m *Iptables) ListPolicies() (PolicyInventory, error) {
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)} inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family) executable := executableForFamily(family)
@@ -218,7 +156,7 @@ func (m *Manager) ListPolicies() (PolicyInventory, error) {
return inventory, nil return inventory, nil
} }
func (m *Manager) Unbind() error { func (m *Iptables) Unbind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.unbindFamily("iptables"); err != nil { if err := m.unbindFamily("iptables"); err != nil {
@@ -232,7 +170,7 @@ func (m *Manager) Unbind() error {
return nil return nil
} }
func (m *Manager) Cleanup() error { func (m *Iptables) Cleanup() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
for _, executable := range []string{"iptables", "ip6tables"} { for _, executable := range []string{"iptables", "ip6tables"} {
@@ -254,7 +192,7 @@ func (m *Manager) Cleanup() error {
return nil return nil
} }
func (m *Manager) Initialized(family string) (bool, error) { func (m *Iptables) Initialized(family string) (bool, error) {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
return false, nil return false, nil
@@ -262,7 +200,7 @@ func (m *Manager) Initialized(family string) (bool, error) {
return m.chainExists(executable, Chain) return m.chainExists(executable, Chain)
} }
func (m *Manager) Status(family string) FamilyStatus { func (m *Iptables) Status(family string) FamilyStatus {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
@@ -278,12 +216,7 @@ func (m *Manager) Status(family string) FamilyStatus {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
} }
status := FamilyStatus{State: StatusNotEffective, Initialized: true} status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules, err := m.run(executable, "-S", DockerChain) jumps := countJumps(chains)
if err != nil {
status.Reason = ReasonInspectFailed
return status
}
jumps := countJumps(rules)
if jumps == 0 { if jumps == 0 {
status.Reason = ReasonJumpMissing status.Reason = ReasonJumpMissing
return status return status
@@ -292,7 +225,7 @@ func (m *Manager) Status(family string) FamilyStatus {
status.Reason = ReasonJumpDuplicate status.Reason = ReasonJumpDuplicate
return status return status
} }
if !hasFirstUniqueJump(rules) { if !hasFirstUniqueJump(chains) {
status.Reason = ReasonJumpNotFirst status.Reason = ReasonJumpNotFirst
return status return status
} }
@@ -302,7 +235,7 @@ func (m *Manager) Status(family string) FamilyStatus {
return status return status
} }
func (m *Manager) bindExistingFamily(executable string, required bool) error { func (m *Iptables) bindExistingFamily(executable string, required bool) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
if required { if required {
return fmt.Errorf("%s is not installed", executable) return fmt.Errorf("%s is not installed", executable)
@@ -315,7 +248,7 @@ func (m *Manager) bindExistingFamily(executable string, required bool) error {
} }
if !chainDeclared(output, DockerChain) { if !chainDeclared(output, DockerChain) {
if required { if required {
return ErrDockerIptablesChainUnavailable return buserr.New("ErrDockerIptablesChainUnavailable")
} }
return nil return nil
} }
@@ -328,7 +261,7 @@ func (m *Manager) bindExistingFamily(executable string, required bool) error {
return m.restoreLifecycle(executable, dockerGuardLifecycleRules(output, true, false)) return m.restoreLifecycle(executable, dockerGuardLifecycleRules(output, true, false))
} }
func (m *Manager) ensureFamily(executable string, required bool) error { func (m *Iptables) ensureFamily(executable string, required bool) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
if required { if required {
return fmt.Errorf("%s is not installed", executable) return fmt.Errorf("%s is not installed", executable)
@@ -341,7 +274,7 @@ func (m *Manager) ensureFamily(executable string, required bool) error {
} }
if !chainDeclared(output, DockerChain) { if !chainDeclared(output, DockerChain) {
if required { if required {
return ErrDockerIptablesChainUnavailable return buserr.New("ErrDockerIptablesChainUnavailable")
} }
return nil return nil
} }
@@ -364,7 +297,7 @@ func dockerGuardLifecycleRules(output string, bind, createOwned bool) [][]string
return rules return rules
} }
func (m *Manager) restoreLifecycle(executable string, rules [][]string) error { func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
@@ -382,7 +315,7 @@ func (m *Manager) restoreLifecycle(executable string, rules [][]string) error {
return nil return nil
} }
func (m *Manager) rebuildLocked(policies []Policy, inventory PolicyInventory) error { func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
@@ -447,7 +380,7 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
continue continue
} }
compiled := compilePolicy(policy) compiled := compilePolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)] orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -524,7 +457,7 @@ func compilePolicy(policy Policy) [][]string {
return rules return rules
} }
func (m *Manager) unbindFamily(executable string) error { func (m *Iptables) unbindFamily(executable string) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
return nil return nil
} }
@@ -538,7 +471,7 @@ func (m *Manager) unbindFamily(executable string) error {
return nil return nil
} }
func (m *Manager) chainExists(executable, chain string) (bool, error) { func (m *Iptables) chainExists(executable, chain string) (bool, error) {
output, err := m.run(executable, "-S") output, err := m.run(executable, "-S")
if err != nil { if err != nil {
return false, err return false, err
@@ -556,7 +489,7 @@ func chainDeclared(output, chain string) bool {
return false return false
} }
func (m *Manager) run(executable string, args ...string) (string, error) { func (m *Iptables) run(executable string, args ...string) (string, error) {
commandArgs := append([]string{"-w", "-t", "filter"}, args...) commandArgs := append([]string{"-w", "-t", "filter"}, args...)
return m.runner.Run(executable, commandArgs...) return m.runner.Run(executable, commandArgs...)
} }
+51 -40
View File
@@ -3,6 +3,7 @@ package docker_guard
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -18,13 +19,13 @@ const (
dockerNftTable = "docker-bridges" dockerNftTable = "docker-bridges"
) )
type NftablesManager struct { type Nftables struct {
runner Runner runner Runner
} }
func NewNftablesManager() *NftablesManager { return &NftablesManager{runner: commandRunner{}} } func NewNftables() *Nftables { return &Nftables{runner: commandRunner{}} }
func (m *NftablesManager) Initialize(policies []Policy) error { func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
@@ -36,10 +37,6 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
if err := m.checkForwardPolicy(); err != nil { if err := m.checkForwardPolicy(); err != nil {
return err return err
} }
inventory, err := m.ListPolicies()
if err != nil {
return err
}
if err := m.ensureFamily(FamilyIPv4, true); err != nil { if err := m.ensureFamily(FamilyIPv4, true); err != nil {
return err return err
} }
@@ -49,7 +46,7 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *NftablesManager) Bind() error { func (m *Nftables) Bind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil { if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
@@ -61,17 +58,13 @@ func (m *NftablesManager) Bind() error {
return nil return nil
} }
func (m *NftablesManager) Reconcile(policies []Policy) error { func (m *Nftables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *NftablesManager) ListPolicies() (PolicyInventory, error) { func (m *Nftables) ListPolicies() (PolicyInventory, error) {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return PolicyInventory{}, nil return PolicyInventory{}, nil
} }
@@ -98,7 +91,7 @@ func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
return inventory, nil return inventory, nil
} }
func (m *NftablesManager) Unbind() error { func (m *Nftables) Unbind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
@@ -109,7 +102,7 @@ func (m *NftablesManager) Unbind() error {
return nil return nil
} }
func (m *NftablesManager) Cleanup() error { func (m *Nftables) Cleanup() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
@@ -126,7 +119,7 @@ func (m *NftablesManager) Cleanup() error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) Initialized(family string) (bool, error) { func (m *Nftables) Initialized(family string) (bool, error) {
if nftTableFamily(family) == "" || !m.runner.Exists("nft") { if nftTableFamily(family) == "" || !m.runner.Exists("nft") {
return false, nil return false, nil
} }
@@ -137,7 +130,7 @@ func (m *NftablesManager) Initialized(family string) (bool, error) {
return m.objectExists("chain", tableFamily, NftTable, NftChain), nil return m.objectExists("chain", tableFamily, NftTable, NftChain), nil
} }
func (m *NftablesManager) Status(family string) FamilyStatus { func (m *Nftables) Status(family string) FamilyStatus {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if tableFamily == "" || !m.runner.Exists("nft") { if tableFamily == "" || !m.runner.Exists("nft") {
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
@@ -145,16 +138,32 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing}
} }
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) || output, err := m.run("-a", "list", "table", tableFamily, NftTable)
!m.objectExists("chain", tableFamily, NftTable, NftChain) { if err != nil {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
}
baseExists, guardExists := false, false
currentChain := ""
var baseRules strings.Builder
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 3 && fields[0] == "chain" && fields[2] == "{" {
currentChain = fields[1]
baseExists = baseExists || currentChain == NftBaseChain
guardExists = guardExists || currentChain == NftChain
} else if strings.TrimSpace(line) == "}" {
currentChain = ""
}
if currentChain == NftBaseChain {
baseRules.WriteString(line)
baseRules.WriteByte('\n')
}
}
if !baseExists || !guardExists {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
} }
status := FamilyStatus{State: StatusNotEffective, Initialized: true} status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain) rules := baseRules.String()
if err != nil {
status.Reason = ReasonInspectFailed
return status
}
jumps := nftJumpHandles(rules) jumps := nftJumpHandles(rules)
if len(jumps) == 0 { if len(jumps) == 0 {
status.Reason = ReasonJumpMissing status.Reason = ReasonJumpMissing
@@ -174,14 +183,14 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
return status return status
} }
func (m *NftablesManager) ensureFamily(family string, required bool) error { func (m *Nftables) ensureFamily(family string, required bool) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if tableFamily == "" { if tableFamily == "" {
return fmt.Errorf("unsupported address family %q", family) return fmt.Errorf("unsupported address family %q", family)
} }
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
if required { if required {
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family) return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
} }
return nil return nil
} }
@@ -213,7 +222,7 @@ func (m *NftablesManager) ensureFamily(family string, required bool) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) bindExistingFamily(family string, required bool) error { func (m *Nftables) bindExistingFamily(family string, required bool) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
if required { if required {
@@ -223,7 +232,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
} }
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
if required { if required {
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family) return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
} }
return nil return nil
} }
@@ -237,7 +246,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
return m.ensureJump(family) return m.ensureJump(family)
} }
func (m *NftablesManager) ensureJump(family string) error { func (m *Nftables) ensureJump(family string) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain) output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
if err != nil { if err != nil {
@@ -251,7 +260,7 @@ func (m *NftablesManager) ensureJump(family string) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) rebuildLocked(policies []Policy, inventory PolicyInventory) error { func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return nil return nil
} }
@@ -312,7 +321,7 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
continue continue
} }
compiled := compileNftPolicy(policy) compiled := compileNftPolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)] orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -408,7 +417,7 @@ func validNftToken(token string) bool {
return !strings.ContainsAny(token, " \t\\\"'") return !strings.ContainsAny(token, " \t\\\"'")
} }
func (m *NftablesManager) unbindFamily(family string) error { func (m *Nftables) unbindFamily(family string) error {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return nil return nil
} }
@@ -427,7 +436,7 @@ func (m *NftablesManager) unbindFamily(family string) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) runBatch(commands [][]string) error { func (m *Nftables) runBatch(commands [][]string) error {
if len(commands) == 0 { if len(commands) == 0 {
return nil return nil
} }
@@ -441,13 +450,13 @@ func (m *NftablesManager) runBatch(commands [][]string) error {
return nil return nil
} }
func (m *NftablesManager) objectExists(kind string, args ...string) bool { func (m *Nftables) objectExists(kind string, args ...string) bool {
command := append([]string{"list", kind}, args...) command := append([]string{"list", kind}, args...)
_, err := m.run(command...) _, err := m.run(command...)
return err == nil return err == nil
} }
func (m *NftablesManager) run(args ...string) (string, error) { func (m *Nftables) run(args ...string) (string, error) {
return m.runner.Run("nft", args...) return m.runner.Run("nft", args...)
} }
@@ -497,9 +506,7 @@ func nftHasFirstUniqueJump(output string) bool {
return false return false
} }
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP") func (m *Nftables) checkForwardPolicy() error {
func (m *NftablesManager) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{ for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4}, {"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6}, {"ip6tables", FamilyIPv6},
@@ -519,7 +526,11 @@ func (m *NftablesManager) checkForwardPolicy() error {
} }
found = true found = true
if fields[2] == "DROP" { if fields[2] == "DROP" {
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop} label := "IPv4"
if family.name == FamilyIPv6 {
label = "IPv6"
}
return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)}
} }
if fields[2] != "ACCEPT" { if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
+7 -159
View File
@@ -1,8 +1,6 @@
package docker_guard package docker_guard
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"sort" "sort"
@@ -12,141 +10,6 @@ import (
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
) )
var ErrInvalidPolicy = errors.New("invalid Docker port guard request")
func NormalizePolicy(policy Policy) (Policy, error) {
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family))
policy.HostIP = strings.TrimSpace(policy.HostIP)
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol))
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode))
if policy.HostPort == 0 ||
(policy.Protocol != "tcp" && policy.Protocol != "udp") ||
(policy.Family != FamilyIPv4 && policy.Family != FamilyIPv6) ||
(policy.Mode != ModeAll && policy.Mode != ModeSources && policy.Mode != ModeAllow) {
return Policy{}, fmt.Errorf("%w: invalid policy fields", ErrInvalidPolicy)
}
address, err := netip.ParseAddr(policy.HostIP)
if err != nil || (policy.Family == FamilyIPv4) != address.Is4() {
return Policy{}, fmt.Errorf("%w: host IP does not match address family", ErrInvalidPolicy)
}
normalizedSources := make([]string, 0, len(policy.Sources))
seen := make(map[string]struct{}, len(policy.Sources))
for _, source := range policy.Sources {
source = strings.TrimSpace(source)
if source == "" {
continue
}
prefix, err := netip.ParsePrefix(source)
if err != nil {
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil {
bits := 128
if sourceAddress.Is4() {
bits = 32
}
prefix = netip.PrefixFrom(sourceAddress, bits)
} else {
return Policy{}, fmt.Errorf("%w: invalid source address %q", ErrInvalidPolicy, source)
}
}
if (policy.Family == FamilyIPv4) != prefix.Addr().Is4() {
return Policy{}, fmt.Errorf("%w: source %q does not match address family", ErrInvalidPolicy, source)
}
canonical := prefix.Masked().String()
if _, exists := seen[canonical]; !exists {
seen[canonical] = struct{}{}
normalizedSources = append(normalizedSources, canonical)
}
}
if policy.Mode != ModeAll && len(normalizedSources) == 0 {
return Policy{}, fmt.Errorf("%w: source-based modes require at least one source", ErrInvalidPolicy)
}
if policy.Mode == ModeAll {
normalizedSources = []string{}
}
sort.Strings(normalizedSources)
policy.Sources = normalizedSources
return policy, nil
}
func NormalizePolicyUUIDs(values []string) ([]string, error) {
uuids := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, policyUUID := range values {
policyUUID = strings.TrimSpace(policyUUID)
if policyUUID == "" {
return nil, fmt.Errorf("%w: policy UUID cannot be empty", ErrInvalidPolicy)
}
if _, exists := seen[policyUUID]; exists {
continue
}
seen[policyUUID] = struct{}{}
uuids = append(uuids, policyUUID)
}
if len(uuids) == 0 {
return nil, fmt.Errorf("%w: policy UUIDs cannot be empty", ErrInvalidPolicy)
}
return uuids, nil
}
func PolicySyncKey(policy Policy) string {
mode := policy.Mode
if mode == ModeAllow && len(policy.Sources) == 0 {
mode = ModeAll
}
sources := make([]string, 0, len(policy.Sources))
for _, source := range policy.Sources {
sources = append(sources, canonicalPolicySource(source))
}
sort.Strings(sources)
return strings.Join([]string{
policy.UUID, policy.Family, CanonicalHost(policy.HostIP), strconv.Itoa(int(policy.HostPort)),
policy.Protocol, mode, strings.Join(sources, ","),
}, "\x00")
}
func canonicalPolicySource(value string) string {
value = strings.TrimSpace(value)
if prefix, err := netip.ParsePrefix(value); err == nil {
return prefix.Masked().String()
}
if address, err := netip.ParseAddr(value); err == nil {
address = address.Unmap()
return netip.PrefixFrom(address, address.BitLen()).String()
}
return value
}
func PolicyStatesEqual(left, right []Policy) bool {
if len(left) != len(right) {
return false
}
counts := make(map[string]int, len(left))
for _, policy := range left {
counts[PolicySyncKey(policy)]++
}
for _, policy := range right {
key := PolicySyncKey(policy)
if counts[key] == 0 {
return false
}
counts[key]--
}
return true
}
func CanonicalHost(value string) string {
if address, err := netip.ParseAddr(value); err == nil {
return address.String()
}
return value
}
func DecodeSources(value string) []string {
result := []string{}
_ = json.Unmarshal([]byte(value), &result)
return result
}
type observedPolicy struct { type observedPolicy struct {
policy Policy policy Policy
sequence int64 sequence int64
@@ -235,7 +98,7 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID) return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
} }
inventory.Policies = append(inventory.Policies, group.policy) inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...) inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...)
} }
return inventory, nil return inventory, nil
} }
@@ -260,12 +123,11 @@ func nativeRuleTokens(tokens []string) []string {
return result return result
} }
func managedOrderKey(family, policyUUID string) string {
return family + "\x00" + policyUUID
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) { func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: wildcardHost(family)} policy := Policy{Family: family, HostIP: "0.0.0.0"}
if family == FamilyIPv6 {
policy.HostIP = "::"
}
source, action := "", "" source, action := "", ""
for index := 0; index < len(tokens); index++ { for index := 0; index < len(tokens); index++ {
switch tokens[index] { switch tokens[index] {
@@ -317,7 +179,7 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1)) policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
} }
case "accept", "drop", "return": case "accept", "drop", "return":
if isCommentValue(tokens, index) { if index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment") {
continue continue
} }
action = tokens[index] action = tokens[index]
@@ -337,20 +199,13 @@ func hasAcceptAction(tokens []string) bool {
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") { if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
return true return true
} }
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) { if strings.EqualFold(token, "accept") && !(index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment")) {
return true return true
} }
} }
return false return false
} }
func isCommentValue(tokens []string, index int) bool {
if index == 0 {
return false
}
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
}
func normalizeObservedHost(value string) string { func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() { if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String() return prefix.Addr().String()
@@ -373,13 +228,6 @@ func parsePolicyPort(value string) uint16 {
return uint16(port) return uint16(port)
} }
func wildcardHost(family string) string {
if family == FamilyIPv6 {
return "::"
}
return "0.0.0.0"
}
func uniqueSortedStrings(values []string) []string { func uniqueSortedStrings(values []string) []string {
seen := make(map[string]struct{}, len(values)) seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values)) result := make([]string, 0, len(values))
+67 -115
View File
@@ -1,15 +1,77 @@
package docker_guard package docker_guard
import ( import (
"errors" "github.com/1Panel-dev/1Panel/agent/buserr"
"fmt" "fmt"
"os" "os"
"slices"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
) )
const (
Chain = "1PANEL_DOCKER"
DockerChain = "DOCKER-USER"
FamilyIPv4 = constant.FirewallFamilyIPv4
FamilyIPv6 = constant.FirewallFamilyIPv6
ModeSources = "deny_sources"
ModeAllow = "allow_sources"
ModeAll = "deny_all"
StatusEffective = "effective"
StatusDisabled = "disabled"
StatusNotEffective = "not_effective"
ReasonCommandMissing = "command_missing"
ReasonDockerChainMissing = "docker_chain_missing"
ReasonGuardChainMissing = "guard_chain_missing"
ReasonJumpMissing = "jump_missing"
ReasonJumpNotFirst = "jump_not_first"
ReasonJumpDuplicate = "jump_duplicate"
ReasonInspectFailed = "inspect_failed"
)
type FamilyError struct {
Family string
Err error
}
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
func (e *FamilyError) Unwrap() error { return e.Err }
type ProxyEndpoint struct {
Protocol string
HostIP string
HostPort uint16
}
type ProxyEndpoints struct {
Items []ProxyEndpoint
Inspected bool
}
type DNATRules struct {
Output string
Inspected bool
}
type Policy struct {
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
}
type FamilyStatus struct {
State string
Reason string
Initialized bool
Bound bool
Effective bool
}
type NativeRule struct { type NativeRule struct {
Family string `json:"family"` Family string `json:"family"`
Order int64 `json:"order"` Order int64 `json:"order"`
@@ -30,9 +92,9 @@ type PolicyInventory struct {
} }
type Runtime interface { type Runtime interface {
Initialize([]Policy) error Initialize([]Policy, PolicyInventory) error
Bind() error Bind() error
Reconcile([]Policy) error ReplacePolicies([]Policy, PolicyInventory) error
Unbind() error Unbind() error
Cleanup() error Cleanup() error
Initialized(string) (bool, error) Initialized(string) (bool, error)
@@ -40,118 +102,8 @@ type Runtime interface {
ListPolicies() (PolicyInventory, error) ListPolicies() (PolicyInventory, error)
} }
func NewRuntime(provider string) Runtime {
if provider == constant.FirewallProviderNftables {
return NewNftablesManager()
}
return NewManager()
}
func Verify(runtime Runtime, desired []Policy, preserved []ReadOnlyPolicy) error {
inventory, err := runtime.ListPolicies()
if err != nil {
return fmt.Errorf("verify synchronized Docker firewall policies: %w", err)
}
if !PolicyStatesEqual(inventory.Policies, desired) {
return fmt.Errorf("verify synchronized Docker firewall policies: target policies do not match the database")
}
if !readOnlyStatesEqual(inventory.ReadOnly, preserved) {
return fmt.Errorf("verify synchronized Docker firewall policies: read-only runtime rules changed")
}
return nil
}
func readOnlyStatesEqual(left, right []ReadOnlyPolicy) bool {
if len(left) != len(right) {
return false
}
leftRules := flattenNativeRules(left)
rightRules := flattenNativeRules(right)
if len(leftRules) != len(rightRules) {
return false
}
for index := range leftRules {
if leftRules[index].Family != rightRules[index].Family || !slices.Equal(leftRules[index].Tokens, rightRules[index].Tokens) {
return false
}
}
return true
}
func flattenNativeRules(policies []ReadOnlyPolicy) []NativeRule {
rules := make([]NativeRule, 0)
for _, policy := range policies {
rules = append(rules, policy.NativeRules...)
}
slices.SortStableFunc(rules, func(left, right NativeRule) int {
if left.Family < right.Family {
return -1
}
if left.Family > right.Family {
return 1
}
if left.Order < right.Order {
return -1
}
if left.Order > right.Order {
return 1
}
return 0
})
return rules
}
func ReconcileTarget(backend string, policies []Policy, runtime Runtime) 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{FamilyIPv4, FamilyIPv6} {
status := runtime.Status(family)
if status.Reason == ReasonInspectFailed {
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
}
initialized = initialized || status.Initialized
}
if initialized {
return runtime.Reconcile(nil)
}
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)
} else {
if needsBind {
err = runtime.Bind()
}
if err == nil {
err = runtime.Reconcile(policies)
}
}
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
}
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward" const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
var ErrIPv4ForwardingDisabled = errors.New("IPv4 forwarding is disabled; set net.ipv4.ip_forward=1 before using Docker's firewall backend")
func CheckIPv4Forwarding() error { func CheckIPv4Forwarding() error {
return checkIPv4Forwarding(os.ReadFile) return checkIPv4Forwarding(os.ReadFile)
} }
@@ -162,7 +114,7 @@ func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error {
return fmt.Errorf("inspect IPv4 forwarding: %w", err) return fmt.Errorf("inspect IPv4 forwarding: %w", err)
} }
if strings.TrimSpace(string(value)) != "1" { if strings.TrimSpace(string(value)) != "1" {
return ErrIPv4ForwardingDisabled return buserr.New("ErrDockerIPv4ForwardingDisabled")
} }
return nil return nil
} }
+18 -41
View File
@@ -21,7 +21,7 @@ const (
ChangeReorder ChangeOperation = "reorder" ChangeReorder ChangeOperation = "reorder"
) )
type DesiredChange struct { type RuleChange struct {
CommandOnly bool `json:"-"` CommandOnly bool `json:"-"`
UnmarkedAdopted bool `json:"-"` UnmarkedAdopted bool `json:"-"`
Operation ChangeOperation `json:"operation"` Operation ChangeOperation `json:"operation"`
@@ -39,7 +39,7 @@ type NativeCommand struct {
Stdin string `json:"stdin,omitempty"` Stdin string `json:"stdin,omitempty"`
} }
type NativeRulePlan struct { type RuleCommands struct {
RuleUUID string `json:"ruleUUID"` RuleUUID string `json:"ruleUUID"`
Operation ChangeOperation `json:"operation"` Operation ChangeOperation `json:"operation"`
Commands []NativeCommand `json:"commands"` Commands []NativeCommand `json:"commands"`
@@ -48,15 +48,14 @@ type NativeRulePlan struct {
Expected ObservedRule `json:"expected"` Expected ObservedRule `json:"expected"`
} }
type BackendPlan struct { type CommandBatch struct {
CommandOnly bool `json:"-"` CommandOnly bool `json:"-"`
Provider Provider `json:"provider"` Provider Provider `json:"provider"`
Scope Scope `json:"scope"` Scope Scope `json:"scope"`
SnapshotRevision string `json:"snapshotRevision"` Rules []RuleCommands `json:"rules"`
Rules []NativeRulePlan `json:"rules"`
} }
func (p BackendPlan) CreatesOnly() bool { func (p CommandBatch) CreatesOnly() bool {
if len(p.Rules) == 0 { if len(p.Rules) == 0 {
return false return false
} }
@@ -68,40 +67,17 @@ func (p BackendPlan) CreatesOnly() bool {
return true return true
} }
type ApplyResult struct {
Applied []ObservedRule `json:"applied"`
Verification *VerifyResult `json:"verification,omitempty"`
}
type VerifyResult struct {
Snapshot Snapshot `json:"snapshot"`
Matched bool `json:"matched"`
}
type Adapter interface { type Adapter interface {
Provider() Provider Provider() Provider
Capabilities(context.Context) (Capabilities, error) Capabilities(context.Context) (Capabilities, error)
Observe(context.Context, Scope) (Snapshot, error) ListRules(context.Context, Scope) (RuleSet, error)
Compile(Snapshot, []DesiredChange) (BackendPlan, error) BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
Apply(context.Context, BackendPlan) (ApplyResult, error) RunCommands(context.Context, CommandBatch) error
Verify(context.Context, BackendPlan) (VerifyResult, error) Rollback(context.Context, CommandBatch) error
} }
type MultiScopeObserver interface { type MultiScopeReader interface {
ObserveScopes(context.Context, []Scope) ([]Snapshot, error) ListRuleScopes(context.Context, []Scope) ([]RuleSet, error)
}
type ObservationSessionFactory interface {
NewObservationSession() Adapter
}
type CreatePlanner interface {
Compile(DesiredChange) (BackendPlan, error)
Applied(ObservedRule)
}
type CreatePlannerFactory interface {
NewCreatePlanner(Snapshot) CreatePlanner
} }
type RulePreparer interface { type RulePreparer interface {
@@ -112,7 +88,8 @@ type RuleChecker interface {
CheckRule(context.Context, FirewallRule) error CheckRule(context.Context, FirewallRule) error
} }
type UnverifiedRuleAppender interface { type ExternalRuleAdapter interface {
ListRulesByComment(context.Context, []Scope, string) ([]ObservedRule, error)
AppendUnverified(context.Context, FirewallRule, string) error AppendUnverified(context.Context, FirewallRule, string) error
} }
@@ -120,6 +97,6 @@ type NativeDetailReader interface {
NativeDetail(context.Context, string, bool) (string, error) NativeDetail(context.Context, string, bool) (string, error)
} }
type PlanRollbacker interface { type RuleSaver interface {
Rollback(context.Context, BackendPlan) error SaveRules(context.Context, Scope) error
} }
+20
View File
@@ -0,0 +1,20 @@
package filter
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type CommentRuleReader interface {
ReadRulesByComment(context.Context, Scope, string) (string, error)
}
func ReadRulesByComment(ctx context.Context, executable string, args []string, comment string) (string, error) {
name, args := cmd.WrapWithOptionalSudo(executable, args...)
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en")).RunPipe(
cmd.PipeCommand{Name: name, Args: args},
cmd.PipeCommand{Name: "sh", Args: []string{"-c", `grep -F -- "$1"; result=$?; if [ "$result" -eq 1 ]; then exit 0; fi; exit "$result"`, "sh", comment}},
)
}
+9 -121
View File
@@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort"
"strings" "strings"
) )
@@ -76,93 +75,6 @@ func SameRuleContent(before, after FirewallRule) (bool, error) {
return err == nil && previous == requested && before.Action == after.Action, err return err == nil && previous == requested && before.Action == after.Action, err
} }
type RuleCollisionIndex map[string][]Action
func (index RuleCollisionIndex) Add(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
index[key] = append(index[key], rule.Action)
return nil
}
func (index RuleCollisionIndex) CheckDuplicate(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if action == rule.Action {
return checkCollisionActions(rule.Action, action)
}
}
return nil
}
func (index RuleCollisionIndex) Check(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if err := checkCollisionActions(rule.Action, action); err != nil {
return err
}
}
return nil
}
func CheckRuleCollision(requested, existing FirewallRule) error {
wanted, err := RuleMatchKey(requested)
if err != nil {
return err
}
actual, err := RuleMatchKey(existing)
if err != nil {
return err
}
if wanted != actual {
return nil
}
return checkCollisionActions(requested.Action, existing.Action)
}
func checkCollisionActions(requested, existing Action) error {
if requested == existing {
return fmt.Errorf("%w: equivalent rule already exists", ErrRuleOperation)
}
if OppositeActions(requested, existing) {
return ErrRuleConflict
}
return nil
}
func CheckObservedRuleCollisions(snapshot Snapshot, requested FirewallRule, excluded *Locator) error {
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported || excluded != nil && SameLocator(observed.Locator, *excluded) {
continue
}
if err := CheckRuleCollision(requested, observed.Rule); err != nil {
return err
}
}
return nil
}
func ObservedRuleCollisionIndex(snapshot Snapshot) (RuleCollisionIndex, error) {
index := make(RuleCollisionIndex, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
continue
}
if err := index.Add(observed.Rule); err != nil {
return nil, err
}
}
return index, nil
}
func normalizedRuleKey(normalized FirewallRule) (string, error) { func normalizedRuleKey(normalized FirewallRule) (string, error) {
identity := ruleIdentity{ identity := ruleIdentity{
Scope: normalized.Scope.Key(), Scope: normalized.Scope.Key(),
@@ -222,49 +134,25 @@ func opaqueInstanceKey(rule ObservedRule, locator Locator) (string, error) {
}{Raw: strings.TrimSpace(rule.Raw), Locator: locator, Persistence: rule.Persistence}) }{Raw: strings.TrimSpace(rule.Raw), Locator: locator, Persistence: rule.Persistence})
} }
func SnapshotRevision(scope Scope, rules []ObservedRule) (string, error) { func NewRuleSet(scope Scope, rules []ObservedRule) (RuleSet, error) {
scope = scope.Normalize() scope = scope.Normalize()
if err := scope.ValidateMVP(); err != nil { if err := scope.ValidateMVP(); err != nil {
return "", err return RuleSet{}, err
} }
identities := make([]string, 0, len(rules))
for _, rule := range rules { for _, rule := range rules {
if rule.Rule.Scope.Normalize().Key() != scope.Key() { if rule.Rule.Scope.Normalize().Key() != scope.Key() {
return "", fmt.Errorf("%w: observed rule scope %q does not match snapshot scope %q", ErrInvalidRule, rule.Rule.Scope.Key(), scope.Key()) return RuleSet{}, fmt.Errorf("%w: observed rule scope does not match read scope", ErrInvalidRule)
} }
if rule.ParseStatus != ParseStatusSupported { if rule.ParseStatus == ParseStatusSupported {
locator, err := validatedLocator(rule.Locator, scope) if _, err := NormalizeRule(rule.Rule); err != nil {
if err != nil { return RuleSet{}, err
return "", err
} }
identity, err := opaqueInstanceKey(rule, locator)
if err != nil {
return "", err
}
identities = append(identities, identity)
continue
} }
if _, err := validatedLocator(rule.Locator, scope); err != nil {
identity, err := InstanceKey(rule) return RuleSet{}, err
if err != nil {
return "", err
} }
identities = append(identities, identity)
} }
sort.Strings(identities) return RuleSet{Scope: scope, Rules: rules}, nil
return hashJSON(struct {
Scope string `json:"scope"`
Rules []string `json:"rules"`
}{Scope: scope.Key(), Rules: identities})
}
func NewSnapshot(scope Scope, rules []ObservedRule) (Snapshot, error) {
revision, err := SnapshotRevision(scope, rules)
if err != nil {
return Snapshot{}, err
}
return Snapshot{Scope: scope.Normalize(), Revision: revision, Rules: rules}, nil
} }
func normalizeLocator(locator Locator, scope Scope) Locator { func normalizeLocator(locator Locator, scope Scope) Locator {
-273
View File
@@ -1,9 +1,6 @@
package filter package filter
import ( import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
) )
@@ -71,273 +68,3 @@ type InventoryMergeInput struct {
Desired []DesiredRule Desired []DesiredRule
ProtectedObservedKeys map[string]struct{} ProtectedObservedKeys map[string]struct{}
} }
type observedInventoryCandidate struct {
rule ObservedRule
ruleKey string
instanceKey string
claimed bool
}
func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) {
candidates := make([]observedInventoryCandidate, len(input.Observed))
byRuleKey := make(map[string][]int)
byInstanceKey := make(map[string][]int)
byMarker := make(map[string][]int)
for index, observed := range input.Observed {
candidate := observedInventoryCandidate{rule: observed}
if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" {
markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker
byMarker[markerKey] = append(byMarker[markerKey], index)
}
if observed.ParseStatus == ParseStatusSupported {
normalized, err := NormalizeRule(observed.Rule)
if err != nil {
return nil, fmt.Errorf("normalize observed firewall rule %d: %w", index, err)
}
candidate.rule.Rule = normalized
candidate.ruleKey, err = normalizedRuleKey(normalized)
if err != nil {
return nil, err
}
byRuleKey[candidate.ruleKey] = append(byRuleKey[candidate.ruleKey], index)
if instanceKey, err := instanceKeyWithRuleKey(candidate.rule, candidate.ruleKey); err == nil {
candidate.instanceKey = instanceKey
candidate.rule.InstanceKey = instanceKey
byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index)
}
}
candidates[index] = candidate
}
normalizedDesired := make([]DesiredRule, 0, len(input.Desired))
desiredMatches := make(map[int]int)
desiredMatchStates := make([]InventoryMatch, 0, len(input.Desired))
for _, desired := range input.Desired {
normalized, err := NormalizeRule(desired.Rule)
if err != nil {
return nil, fmt.Errorf("normalize desired firewall rule %q: %w", desired.UUID, err)
}
desired.Rule = normalized
calculatedKey, err := normalizedRuleKey(normalized)
if err != nil {
return nil, err
}
if desired.RuleKey != "" && desired.RuleKey != calculatedKey {
return nil, fmt.Errorf("%w: desired rule %q key does not match its semantics", ErrInvalidRule, desired.UUID)
}
desired.RuleKey = calculatedKey
match, matchState := findObservedInventoryMatch(desired, candidates, byRuleKey, byInstanceKey, byMarker)
normalizedIndex := len(normalizedDesired)
normalizedDesired = append(normalizedDesired, desired)
desiredMatchStates = append(desiredMatchStates, matchState)
if match >= 0 {
candidates[match].claimed = true
desiredMatches[match] = normalizedIndex
}
}
items := make([]InventoryItem, 0, len(candidates)+len(normalizedDesired))
matchedDesired := make(map[int]struct{}, len(desiredMatches))
for index := range candidates {
candidate := &candidates[index]
if desiredIndex, exists := desiredMatches[index]; exists {
desired := normalizedDesired[desiredIndex]
observed := candidate.rule
match := desiredMatchStates[desiredIndex]
if match == InventoryMatchExact && observed.ParseStatus != ParseStatusSupported {
orderIndex := observed.Rule.OrderIndex
observed.Rule = desired.Rule
observed.Rule.OrderIndex = orderIndex
observed.ParseStatus = ParseStatusSupported
observed.UncertainFields = nil
}
displayRule := observed.Rule
displayRule.Description = desired.Rule.Description
state := inventoryStateForDesired(desired, match)
if observed.Protected {
state = InventoryStateProtected
} else if observed.Persistence != "" && observed.Persistence != PersistenceStatusConverged {
state = InventoryStateDrifted
}
items = append(items, InventoryItem{
Rule: displayRule,
Observed: &observed,
Desired: &desired,
State: state,
Match: match,
})
matchedDesired[desiredIndex] = struct{}{}
continue
}
observed := candidate.rule
state := InventoryStateExternal
if observed.Protected {
state = InventoryStateProtected
} else if _, protected := input.ProtectedObservedKeys[candidate.ruleKey]; protected {
state = InventoryStateProtected
}
match := InventoryMatchNone
if observed.ParseStatus != ParseStatusSupported {
match = InventoryMatchOpaque
}
items = append(items, InventoryItem{Rule: observed.Rule, Observed: &observed, State: state, Match: match})
}
for index, desired := range normalizedDesired {
if _, matched := matchedDesired[index]; matched {
continue
}
desiredCopy := desired
match := desiredMatchStates[index]
items = append(items, InventoryItem{
Rule: desired.Rule,
Desired: &desiredCopy,
State: inventoryStateForDesired(desired, match),
Match: match,
})
}
return items, nil
}
func findObservedInventoryMatch(
desired DesiredRule,
candidates []observedInventoryCandidate,
byRuleKey map[string][]int,
byInstanceKey map[string][]int,
byMarker map[string][]int,
) (int, InventoryMatch) {
if marker := strings.TrimSpace(desired.Marker); marker != "" {
markerKey := desired.Rule.Scope.Key() + "\x00" + marker
match, status := uniqueUnclaimedCandidate(byMarker[markerKey], candidates)
if match >= 0 && candidates[match].rule.ParseStatus != ParseStatusOpaque &&
!ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) {
return match, InventoryMatchChanged
}
if status != InventoryMatchMissing {
return match, status
}
if desired.Origin == RuleOriginAdopted {
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, "", candidates)
if status != InventoryMatchMissing {
if match >= 0 {
return match, InventoryMatchChanged
}
return match, status
}
}
legacyMarker := "1panel-rule:" + strings.TrimSpace(desired.UUID)
if legacyMarker != "1panel-rule:" && legacyMarker != marker {
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, legacyMarker, candidates)
if status != InventoryMatchMissing {
if match >= 0 {
return match, InventoryMatchChanged
}
return match, status
}
}
return match, status
}
if desired.ObservedInstanceKey != "" {
return uniqueUnclaimedCandidate(byInstanceKey[desired.ObservedInstanceKey], candidates)
}
return uniqueUnclaimedCandidate(byRuleKey[desired.RuleKey], candidates)
}
func uniqueUnclaimedSemanticCandidate(
expected FirewallRule,
marker string,
candidates []observedInventoryCandidate,
) (int, InventoryMatch) {
match := -1
count := 0
for index := range candidates {
candidate := candidates[index]
if candidate.claimed || strings.TrimSpace(candidate.rule.Marker) != marker ||
!ObservedRuleMatchesExpected(candidate.rule, expected) {
continue
}
match = index
count++
}
switch count {
case 0:
return -1, InventoryMatchMissing
case 1:
return match, InventoryMatchExact
default:
return -1, InventoryMatchAmbiguous
}
}
func uniqueUnclaimedCandidate(indices []int, candidates []observedInventoryCandidate) (int, InventoryMatch) {
match := -1
count := 0
for _, index := range indices {
if candidates[index].claimed {
continue
}
match = index
count++
}
switch count {
case 0:
return -1, InventoryMatchMissing
case 1:
return match, InventoryMatchExact
default:
return -1, InventoryMatchAmbiguous
}
}
func inventoryStateForDesired(desired DesiredRule, match InventoryMatch) InventoryState {
if match != InventoryMatchExact {
return InventoryStateDrifted
}
if desired.Protected {
return InventoryStateProtected
}
switch desired.Origin {
case RuleOriginAdopted:
return InventoryStateAdopted
default:
return InventoryStateManaged
}
}
func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) {
if provider == ProviderFirewalld {
return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767}
}
for _, item := range items {
if item.Observed == nil || item.Observed.Locator.Position == nil {
continue
}
scope := item.Observed.Rule.Scope
if scope.Provider != provider || scope.Direction != DirectionInput {
continue
}
if (provider == ProviderIptables || provider == ProviderNftables) &&
(scope.Table != "filter" || scope.Chain != IptablesInputChain) {
continue
}
bounds := &ipv4
if scope.Family == FamilyIPv6 {
bounds = &ipv6
} else if scope.Family != FamilyIPv4 {
continue
}
position := *item.Observed.Locator.Position
if position < 1 {
continue
}
if bounds.Min == 0 || position < bounds.Min {
bounds.Min = position
}
bounds.Max = max(bounds.Max, position)
if provider != ProviderUFW {
bounds.Min = 1
}
}
return
}
+11 -12
View File
@@ -110,13 +110,12 @@ type ScopeNotice struct {
} }
var ( var (
ErrInvalidScope = errors.New("invalid firewall scope") ErrInvalidScope = errors.New("invalid firewall scope")
ErrUnsupportedScope = errors.New("unsupported firewall scope") ErrUnsupportedScope = errors.New("unsupported firewall scope")
ErrManagedScopeChange = fmt.Errorf("%w: managed rule scope cannot be changed", ErrUnsupportedScope) ErrInvalidRule = errors.New("invalid firewall rule")
ErrInvalidRule = errors.New("invalid firewall rule") ErrProtectedRule = errors.New("protected firewall rule cannot be modified")
ErrProtectedRule = errors.New("protected firewall rule cannot be modified") ErrCompositeRule = errors.New("firewall rule must be atomic")
ErrCompositeRule = errors.New("firewall rule must be atomic") ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
) )
type Scope struct { type Scope struct {
@@ -298,11 +297,11 @@ type ObservedRule struct {
Persistence PersistenceStatus `json:"persistence,omitempty"` Persistence PersistenceStatus `json:"persistence,omitempty"`
} }
type Snapshot struct { type RuleSet struct {
Scope Scope `json:"scope"` LastPosition int `json:"-"`
Revision string `json:"revision"` Scope Scope `json:"scope"`
Rules []ObservedRule `json:"rules"` Rules []ObservedRule `json:"rules"`
Notices []ScopeNotice `json:"notices,omitempty"` Notices []ScopeNotice `json:"notices,omitempty"`
} }
type Capabilities struct { type Capabilities struct {
+7 -19
View File
@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
const MaxAtomicExpansion = 256 const MaxAtomicExpansion = 500
func NormalizeRule(rule FirewallRule) (FirewallRule, error) { func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
rule.Scope = rule.Scope.Normalize() rule.Scope = rule.Scope.Normalize()
@@ -16,9 +16,9 @@ func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
return FirewallRule{}, err return FirewallRule{}, err
} }
if hasCompositeValue(rule.SourceAddress) || hasCompositeValue(rule.DestinationAddress) || if strings.Contains(rule.SourceAddress, ",") || strings.Contains(rule.DestinationAddress, ",") ||
hasCompositeValue(rule.SourcePort) || strings.Contains(rule.SourcePort, ",") ||
(hasCompositeValue(rule.DestinationPort) && !supportsNativeDestinationPortSet(rule.Scope.Provider)) || (strings.Contains(rule.DestinationPort, ",") && rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderUFW) ||
isCompositeProtocol(rule.Protocol) { isCompositeProtocol(rule.Protocol) {
return FirewallRule{}, fmt.Errorf("%w: expand addresses, ports and protocols before normalization", ErrCompositeRule) return FirewallRule{}, fmt.Errorf("%w: expand addresses, ports and protocols before normalization", ErrCompositeRule)
} }
@@ -37,11 +37,11 @@ func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: destination address: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: destination address: %v", ErrInvalidRule, err)
} }
rule.SourcePort, err = normalizePort(rule.SourcePort) rule.SourcePort, err = normalizePortValue(rule.SourcePort, false)
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: source port: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: source port: %v", ErrInvalidRule, err)
} }
rule.DestinationPort, err = normalizePortValue(rule.DestinationPort, supportsNativeDestinationPortSet(rule.Scope.Provider)) rule.DestinationPort, err = normalizePortValue(rule.DestinationPort, rule.Scope.Provider == ProviderIptables || rule.Scope.Provider == ProviderUFW)
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: destination port: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: destination port: %v", ErrInvalidRule, err)
} }
@@ -136,7 +136,7 @@ func ExpandAtomicRules(input FirewallRule) ([]FirewallRule, error) {
destinationAddresses := splitValues(input.DestinationAddress) destinationAddresses := splitValues(input.DestinationAddress)
sourcePorts := splitValues(input.SourcePort) sourcePorts := splitValues(input.SourcePort)
destinationPorts := splitValues(input.DestinationPort) destinationPorts := splitValues(input.DestinationPort)
if supportsNativeDestinationPortSet(input.Scope.Provider) { if input.Scope.Provider == ProviderIptables || input.Scope.Provider == ProviderUFW {
destinationPorts = []string{input.DestinationPort} destinationPorts = []string{input.DestinationPort}
} }
@@ -272,10 +272,6 @@ func validateAddressFamily(address netip.Addr, family Family) error {
return nil return nil
} }
func normalizePort(value string) (string, error) {
return normalizePortValue(value, false)
}
func normalizePortValue(value string, allowSet bool) (string, error) { func normalizePortValue(value string, allowSet bool) (string, error) {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" || strings.EqualFold(value, "any") || strings.EqualFold(value, "anywhere") { if value == "" || strings.EqualFold(value, "any") || strings.EqualFold(value, "anywhere") {
@@ -348,10 +344,6 @@ func normalizePortValue(value string, allowSet bool) (string, error) {
return fmt.Sprintf("%d-%d", start, end), nil return fmt.Sprintf("%d-%d", start, end), nil
} }
func supportsNativeDestinationPortSet(provider Provider) bool {
return provider == ProviderIptables || provider == ProviderUFW
}
func parsePort(value string) (int, error) { func parsePort(value string) (int, error) {
port, err := strconv.Atoi(strings.TrimSpace(value)) port, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || port < 1 || port > 65535 { if err != nil || port < 1 || port > 65535 {
@@ -404,10 +396,6 @@ func normalizeConnectionStates(values []string, provider Provider) ([]string, er
return states, nil return states, nil
} }
func hasCompositeValue(value string) bool {
return strings.Contains(value, ",")
}
func isCompositeProtocol(value string) bool { func isCompositeProtocol(value string) bool {
value = strings.ToLower(strings.TrimSpace(value)) value = strings.ToLower(strings.TrimSpace(value))
return value == "tcp/udp" || value == "udp/tcp" || strings.Contains(value, ",") return value == "tcp/udp" || value == "udp/tcp" || strings.Contains(value, ",")

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