Compare commits

...
Author SHA1 Message Date
guoyuqi ade1accd3f fix(frontend): reset search filters and pagination on page visits 2026-10-09 17:42:19 +08:00
ssongliu ce14a104cc feat(vm): move shared VM support to agent (#14007) 2026-10-09 14:09:53 +08:00
CN_CoreStebandssongliu 2eaabccf94 fix(dashboard): prevent overlapping df collection and share disk queries (#13971)
* fix(dashboard): keep disk info working when a mount is stale or broken

The dashboard, the file manager mount list and the alert disk list
enumerated mounts with df. df calls statfs on every mount, so one bad
mount took the whole disk list down: the request hung until the
frontend timed out, or the list came back empty.

- enumerate mounts from /proc/self/mountinfo instead of running df
- read each mount through a guard with a 3s timeout that keeps at most
  one call per mount in flight
- fail closed: a mount that cannot be read stays in the list with
  errorType and error, and an unreadable mount table is an error
  rather than an empty list
- show failed mounts and their reasons on the dashboard and in the
  file manager
- cmd: after killing a timed-out command, wait at most 5s for it
  instead of forever

* fix(dashboard): distinguish remounted and hidden filesystems

* fix(disk): simplify disk queries and bound timeout handling

* fix(disk): prevent overlapping df collection tasks

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 10:14:16 +08:00
ssongliu e119239d3d refactor: manage firewall rules through runtime state and file backups (#13998)
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations.

Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
2026-10-08 18:19:30 +08:00
ssongliu 14a57af4e9 feat(vm): clarify network modes and add macvtap translations (#13996) 2026-10-08 18:18:17 +08:00
Yuki Guoandssongliu 2701af9054 refactor: select bind (#13993)
* refactor: select bind

* refactor: simplify table selection state handling

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-08 17:11:28 +08:00
Yuki Guoandssongliu 4954067736 refactor: search bind (#13992)
* refactor: search bind

* refactor: simplify search persistence

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-08 16:19:29 +08:00
ssongliu b962144e7d fix(ssh): validate imported key pairs and cancel stale file reads (#13991) 2026-10-08 14:06:40 +08:00
Yuki Guo 8f1be42b6b fix: fix log text (#13990) 2026-10-08 11:32:09 +08:00
Yuki Guo c6969f231b fix: fix operation log (#13989) 2026-10-08 11:08:04 +08:00
4kerccand4kercc 3571fb8d86 fix: speed up SFTP backup upload with concurrent writes (#13976)
The SFTP uploader used io.Copy(dstFile, srcFile). Since the source is an
*os.File it implements io.WriterTo, so io.Copy took the os.File.WriteTo
branch and issued one synchronous SFTP WRITE per packet, waiting for the
server reply every time. Throughput was therefore bounded by packetSize/RTT
instead of the link capacity: on a ~68ms RTT link a 440MB backup reached
only ~0.25 MB/s, while rclone / OpenSSH sftp reach 16-19 MB/s on the very
same link and target.

pkg/sftp already provides a pipelined write path (File.ReadFrom, enabled by
UseConcurrentWrites and bounded by MaxConcurrentRequestsPerFile, default 64).
Use it for uploads, and truncate the remote file to the number of bytes
actually written when the concurrent write fails, to avoid leaving holes.

Measured against the same 68ms RTT target: a 96MiB upload went from 6m40s
(~0.25 MB/s) to ~6s (~16 MB/s); a 950MB three-site backup job from ~2h40m to
~2m04s.

Co-authored-by: 4kercc <13767585+4kercc@users.noreply.github.com>
2026-10-08 09:45:51 +08:00
ssongliu d21288788e fix: harden WebSocket origin validation (#13953) 2026-09-30 15:21:21 +08:00
ssongliu f65e4b06ab feat(vm): add VNC keyboard control translations (#13951)
* feat(vm): add VNC keyboard control translations

* feat(vm): add Windows VirtIO driver guidance translations

* feat(vm): add disk expansion translations and operation logs
2026-09-30 15:20:19 +08:00
ssongliu d26bc9a3a4 chore(deps): update dependencies and address security advisories (#13945)
Update pending Go and frontend dependencies, including gRPC, OpenTelemetry
and SVGO security fixes. Raise the minimum Go version to 1.26.6 and update
x/image and x/mod to patched versions.

Use moby/go-archive directly for image build contexts so its vulnerable
v0.1.0 replacement can be removed and v0.3.0 used. Regenerate Go module
checksums with go mod tidy and refresh the npm lockfile.

Validation: Linux amd64 builds, existing Go tests and go mod verify pass
for core and agent; frontend production build passes; npm audit reports
zero vulnerabilities. Frontend type-check reports the same nine errors
with the original and updated lockfiles.

Go scanning still flags two advisories in the legacy Docker SDK, which
has no patched release on its current module path. A separate SDK
migration and host Docker daemon updates remain necessary.
2026-09-29 21:36:19 +08:00
ssongliu 7088a903fe fix(vllm): sample realtime metrics independently of history (#13944) 2026-09-29 17:29:29 +08:00
ssongliu 864ebeed82 fix: load domestic release notes from new docs (#13943) 2026-09-29 17:29:16 +08:00
CN_CoreStebandssongliu c1b2b92708 feat(monitor): add monitor data export with csv, json and xlsx formats (#13875)
- new export tab between monitor and settings with time range, metric,
  device/interface/gpu selection and format choice
- files are generated in the browser from existing monitor apis; no new
  backend endpoints
- csv with utf-8 bom, escaping and formula sanitization; multiple metrics
  are zipped; xlsx uses one worksheet per metric with real date cells and
  row-limit warnings; json emits flat row objects
- share downloadBlob in utils/file with delayed url revocation
- use the 60s monitor timeout and node header for gpu queries

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-09-29 15:50:00 +08:00
CN_CoreStebandssongliu 2189d9229a perf(monitor): improve monitor query and chart performance for large ranges (#13874)
* perf(monitor): improve monitor query and chart performance for large ranges

- enable LTTB sampling in the shared line chart to bound rendered points
- empty io/network in monitor search now means no name filter, returning
  all device rows in one query instead of fanning out per device
- merge device names recorded in the monitor db into io/network/gpu
  options so removed devices stay selectable; move logic to service layer
- add composite indexes (name, created_at) on monitor_ios/monitor_networks
  and (product_name, created_at) on monitor_gpus
- stop migrating the unused MonitorGPU model into MonitorDB
- fix the memory param which was compared against "mem" and never matched

* chore(monitor): remove monitor test fixtures

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-09-29 15:34:16 +08:00
ssongliu d0183f460e feat: add vLLM monitoring (#13942) 2026-09-29 15:19:30 +08:00
ssongliu d27d6db1ea feat: enhance native GPU NPU and XPU monitoring (#13939) 2026-09-28 18:32:12 +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
ssongliu 4712ceaf2c fix: allow read-only firewall APIs in demo mode (#13914) 2026-09-24 14:01:05 +08:00
261 changed files with 15929 additions and 13128 deletions
+79 -138
View File
@@ -4,11 +4,9 @@ import (
"errors"
"github.com/1Panel-dev/1Panel/agent/buserr"
"net/http"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
@@ -90,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/firewall/forward/base [post]
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
data, err := forwardingService.LoadBaseInfo()
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
if err != nil {
helper.InternalServer(c, err)
return
@@ -111,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
total, items, err := forwardingService.SearchRules(request)
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
if err != nil {
helper.InternalServer(c, err)
return
@@ -262,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
helper.SuccessWithData(c, info)
}
// @Tags Firewall
// @Summary Adopt an external firewall rule
// @Accept json
// @Param request body dto.FirewallRuleAdopt true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/adopt [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
var request dto.FirewallRuleAdopt
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Queue firewall rule creation
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
@@ -309,69 +285,9 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Preview firewall rule synchronization
// @Accept json
// @Param request body dto.FirewallRuleSyncRequest true "request"
// @Success 200 {object} dto.FirewallRuleSyncPreview
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync/preview [post]
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
var request dto.FirewallRuleSyncRequest
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Load the currently executing firewall rule synchronization task
// @Success 200 {object} dto.FirewallRuleSyncTask
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync/task [get]
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
result, err := firewallService.CurrentRuleSyncTask()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Synchronize firewall rules to a target backend
// @Accept json
// @Param request body dto.FirewallRuleSyncRequest true "request"
// @Success 200 {object} dto.FirewallRuleSyncResult
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync [post]
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
var request dto.FirewallRuleSyncRequest
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Queue firewall rule deletion
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
// @Accept json
// @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -394,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
}
// @Tags Firewall
// @Summary Update a managed unified firewall v2 rule
// @Summary Update a firewall rule
// @Accept json
// @Param request body dto.FirewallRuleUpdate true "request"
// @Success 200
@@ -402,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/update [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"}
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
var request dto.FirewallRuleUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if !normalizeFirewallRuleUUID(c, &request.UUID) {
return
}
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
if err := firewallService.Update(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
@@ -419,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
}
// @Tags Firewall
// @Summary Reorder a managed unified firewall v2 rule
// @Summary Reorder a firewall rule
// @Accept json
// @Param request body dto.FirewallRuleReorder true "request"
// @Success 200
@@ -427,35 +340,19 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/reorder [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"}
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
var request dto.FirewallRuleReorder
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if !normalizeFirewallRuleUUID(c, &request.UUID) {
return
}
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
}
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
if value == nil {
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
return false
}
*value = strings.TrimSpace(*value)
if *value == "" {
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
return false
}
return true
}
func handleFirewallRuleError(c *gin.Context, err error) {
var businessErr buserr.BusinessError
isBusinessError := errors.As(err, &businessErr)
@@ -464,21 +361,20 @@ func handleFirewallRuleError(c *gin.Context, err error) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrRuleStale):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
case isBusinessError && businessErr.Msg == "ErrRecordExist":
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
c.Abort()
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
c.Abort()
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
c.Abort()
case errors.Is(err, filter.ErrVerificationFailed):
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
default:
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
}
@@ -501,7 +397,7 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
// @Tags Firewall
// @Summary Create firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200
@@ -523,7 +419,7 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
// @Tags Firewall
// @Summary Update firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200
@@ -545,7 +441,7 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
// @Tags Firewall
// @Summary Delete firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Description Removes whitelist configuration; existing firewall rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200
@@ -622,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Sync Docker port guard rules
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/sync [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Operate Docker port guard
// @Accept json
@@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) {
}
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
}
// @Tags Firewall
// @Summary List firewall rule backups
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
// @Success 200 {object} dto.FirewallRuleBackups
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/backups [get]
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Initialize, repair or bind one firewall address family
// @Accept json
// @Param request body dto.FirewallFamilyOperation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/family/operate [post]
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
var request dto.FirewallFamilyOperation
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.OperateFamily(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Update firewall IPv6 support
// @Accept json
// @Param request body dto.FirewallIPv6Operation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/ipv6 [post]
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
var request dto.FirewallIPv6Operation
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.OperateIPv6(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+10 -23
View File
@@ -1,13 +1,9 @@
package v2
import (
"sort"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/net"
)
// @Tags Monitor
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
}
// @Tags Monitor
// @Summary Clean monitor data
// @Summary Clean host or GPU monitor data
// @Param request body dto.MonitorClean true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
func (b *BaseApi) CleanMonitor(c *gin.Context) {
if err := monitorService.CleanData(); err != nil {
var req dto.MonitorClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanData(req.Type); err != nil {
helper.InternalServer(c, err)
return
}
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/netoptions [get]
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
netStat, _ := net.IOCounters(true)
var options []string
options = append(options, "all")
for _, net := range netStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
}
// @Tags Monitor
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/iooptions [get]
func (b *BaseApi) GetIOOptions(c *gin.Context) {
diskStat, _ := disk.IOCounters()
var options []string
options = append(options, "all")
for _, net := range diskStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadIOOptions())
}
+45
View File
@@ -0,0 +1,45 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
var req dto.MonitorVLLMCurrent
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanVLLMMonitor(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+1 -1
View File
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @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) {
var req request.RuntimeOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+2
View File
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.CreateRootCert(req); err != nil {
helper.InternalServer(c, err)
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.EditRootCert(req); err != nil {
helper.InternalServer(c, err)
+2
View File
@@ -21,6 +21,7 @@ type AlertBase struct {
}
type PushAlert struct {
Result string `json:"result,omitempty"`
TaskName string `json:"taskName"`
AlertType string `json:"alertType"`
EntryID uint `json:"entryID"`
@@ -53,6 +54,7 @@ type AlertDTO struct {
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
TaskName string `json:"taskName,omitempty"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
+10 -7
View File
@@ -51,9 +51,10 @@ type CronjobOperate struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
CleanLogConfig
}
@@ -126,7 +127,8 @@ type CronjobInfo struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertCount uint `json:"alertCount"`
AlertTriggerMode string `json:"alertTriggerMode"`
}
type CronjobImport struct {
@@ -169,9 +171,10 @@ type CronjobTrans struct {
SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
}
type TransHelper struct {
Name string `json:"name"`
+86 -121
View File
@@ -2,11 +2,13 @@ package dto
import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
)
type FirewallSubsystemStatus struct {
IPv6Enabled bool `json:"ipv6Enabled"`
Name string `json:"name"`
Backend string `json:"backend"`
ConflictBackend string `json:"conflictBackend,omitempty"`
@@ -18,7 +20,6 @@ type FirewallSubsystemStatus struct {
PingStatus string `json:"pingStatus"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
@@ -49,6 +50,7 @@ type FirewallBackendOption struct {
}
type FirewallBackendFamilyStatus struct {
Partial bool `json:"partial"`
Available bool `json:"available"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
@@ -64,6 +66,7 @@ type FirewallBackendGroup struct {
}
type FirewallSettings struct {
IPv6Enabled bool `json:"ipv6Enabled"`
System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"`
@@ -92,6 +95,17 @@ type FirewallBackendOperation struct {
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
}
type FirewallIPv6Operation struct {
Status string `json:"status" validate:"required,oneof=Enable Disable"`
}
type FirewallFamilyOperation struct {
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
}
type FilterChainOperation struct {
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
@@ -104,42 +118,55 @@ type FilterChainOperationResponse struct {
}
type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallSystemPort = firewall.SystemPort
type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"`
IPv6Range filter.PositionRange `json:"ipv6Range"`
Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"`
ManagedTotal int64 `json:"managedTotal"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
IPv4Range filter.PositionRange `json:"ipv4Range"`
IPv6Range filter.PositionRange `json:"ipv6Range"`
Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
}
type FirewallRuleBackup struct {
Name string `json:"name"`
Provider filter.Provider `json:"provider"`
RuleCount int `json:"ruleCount"`
ModifiedAt int64 `json:"modifiedAt"`
}
type FirewallRuleBackups struct {
Directory string `json:"directory"`
Files []FirewallRuleBackup `json:"files"`
}
type FirewallRuleResetResponse struct {
Removed int `json:"removed"`
Disabled bool `json:"disabled"`
BackupPath string `json:"backupPath"`
Removed int `json:"removed"`
Disabled bool `json:"disabled"`
}
type FirewallRuleReset struct {
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
Backup *bool `json:"backup,omitempty" default:"true"`
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
PageInfo
Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
All bool `json:"all,omitempty"`
Info string `json:"info"`
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
All bool `json:"all,omitempty"`
Info string `json:"info"`
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
}
type FirewallNativeDetail struct {
@@ -150,6 +177,7 @@ type FirewallNativeDetail struct {
}
type DockerPortGuardBase struct {
IPv6Enabled bool `json:"ipv6Enabled"`
Name string `json:"name"`
Version string `json:"version"`
IsExist bool `json:"isExist"`
@@ -162,6 +190,7 @@ type DockerPortGuardBase struct {
}
type DockerPortGuardFamilyStatus struct {
Partial bool `json:"partial"`
State string `json:"state"`
Reason string `json:"reason,omitempty"`
Initialized bool `json:"initialized"`
@@ -182,11 +211,8 @@ type DockerPortGuardEndpoint struct {
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"`
@@ -223,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct {
type DockerPortGuardPolicyBatch struct {
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
Import bool `json:"import"`
}
type DockerPortGuardPolicyBatchDelete struct {
@@ -231,31 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct {
type DockerPortGuardPolicy struct {
DockerPortGuardEndpointIdentity
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
}
type DockerPortGuardOperation struct {
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
Marker string `json:"marker,omitempty" validate:"max=256"`
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
Raw string `json:"raw,omitempty"`
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
}
type FirewallRuleCreate struct {
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
Initialize bool `json:"initialize"`
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
}
type FirewallRuleCreateResponse struct {
@@ -274,66 +297,13 @@ type FirewallRuleCreateFailure struct {
Error string `json:"error,omitempty"`
}
type FirewallRuleSyncRequest struct {
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
ResetSource bool `json:"resetSource"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleSyncItem struct {
SourceUUID string `json:"sourceUUID"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
Status firewallsync.Status `json:"status"`
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
Reason string `json:"reason,omitempty"`
}
type FirewallRuleSyncPreview struct {
Subsystem string `json:"subsystem"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
TargetProvider filter.Provider `json:"targetProvider"`
Total int `json:"total"`
Ready int `json:"ready"`
Existing int `json:"existing"`
Removed int `json:"removed"`
Blocked int `json:"blocked"`
Items []FirewallRuleSyncItem `json:"items"`
}
type FirewallRuleSyncResult struct {
Subsystem string `json:"subsystem"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
TargetProvider filter.Provider `json:"targetProvider"`
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Skipped int `json:"skipped"`
Removed int `json:"removed"`
Failed int `json:"failed"`
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
}
type FirewallRuleSyncTask struct {
TaskID string `json:"taskID,omitempty"`
Executing bool `json:"executing"`
}
type FirewallRuleSyncFailure struct {
SourceUUID string `json:"sourceUUID"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
Error string `json:"error"`
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
}
type FirewallRuleDeleteItem struct {
FirewallRuleDeleteTarget
Observed filter.ObservedRule `json:"observed" validate:"required"`
}
type FirewallRuleDeleteTarget struct {
@@ -350,13 +320,13 @@ type FirewallRuleDeleteResponse struct {
}
type FirewallRuleDeleteFailure struct {
Index int `json:"index"`
UUID string `json:"uuid"`
Error string `json:"error"`
Index int `json:"index"`
InstanceKey string `json:"instanceKey"`
Error string `json:"error"`
}
type FirewallRuleUpdate struct {
UUID string `json:"uuid" validate:"required,max=64"`
FirewallRuleDeleteTarget
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
@@ -364,26 +334,21 @@ type FirewallRuleUpdate struct {
}
type FirewallRuleReorder struct {
UUID string `json:"uuid" validate:"required,max=64"`
FirewallRuleDeleteTarget
TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"`
}
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
p.Items = append(p.Items, item)
switch item.Status {
case firewallsync.StatusReady:
p.Ready++
p.Total++
case firewallsync.StatusExisting:
p.Existing++
p.Total++
case firewallsync.StatusRemove:
p.Removed++
case firewallsync.StatusBlocked:
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
type FirewallRuleExportItem struct {
filter.FirewallRule
Raw string `json:"raw,omitempty"`
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
}
type FirewallSubsystemBackup struct {
Families []string `json:"families,omitempty"`
Subsystem string `json:"subsystem"`
Provider filter.Provider `json:"provider"`
Forwarding []forwarding.Rule `json:"forwarding"`
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
}
+2 -6
View File
@@ -24,15 +24,11 @@ type ForwardRule struct {
UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
IsDesired bool `json:"isDesired"`
IsRuntime bool `json:"isRuntime"`
SyncStatus string `json:"syncStatus"`
}
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
Import bool `json:"import"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
}
type ForwardRuleOperation struct {
+87 -18
View File
@@ -1,6 +1,10 @@
package dto
import "time"
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type MonitorSearch struct {
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
@@ -26,24 +30,33 @@ type Process struct {
}
type MonitorSetting struct {
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
GPUMonitorStatus string `json:"gpuMonitorStatus"`
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
GPUMonitorInterval string `json:"gpuMonitorInterval"`
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
}
type MonitorSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
Value string `json:"value"`
}
type MonitorGPUOptions struct {
Supported bool `json:"supported"`
GPUType string `json:"gpuType"`
ChartHide []GPUChartHide `json:"chartHide"`
Options []string `json:"options"`
}
type GPUChartHide struct {
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
Type string `json:"type"`
Process bool `json:"process"`
@@ -55,23 +68,54 @@ type GPUChartHide struct {
Speed bool `json:"speed"`
}
type MonitorGPUSearch struct {
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type MonitorGPUData struct {
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 []int `json:"speedValue"`
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"`
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"`
}
@@ -81,3 +125,28 @@ type GPUProcess struct {
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}
type MonitorVLLMSearch struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"`
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
}
type MonitorVLLMData struct {
SampleCount int64 `json:"sampleCount"`
BucketSeconds int64 `json:"bucketSeconds"`
Points []model.MonitorVLLM `json:"points"`
}
type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorClean struct {
Type string `json:"type" validate:"required,oneof=host gpu"`
}
+1 -1
View File
@@ -25,7 +25,7 @@ type RootCertOperate struct {
ID uint `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
EncryptionMode string `json:"encryptionMode"`
PassPhrase string `json:"passPhrase"`
PublicKey string `json:"publicKey"`
PrivateKey string `json:"privateKey"`
-51
View File
@@ -1,51 +0,0 @@
package model
type DockerPortGuardPolicy struct {
BaseModel
UUID string `gorm:"uniqueIndex" json:"uuid"`
ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
Mode string `json:"mode"`
Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"`
NativeAction string `gorm:"default:''" json:"-"`
NativeRules string `gorm:"type:text" json:"-"`
Sequence int64 `gorm:"default:0" json:"-"`
}
type ForwardingRule struct {
BaseModel
Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
}
type FirewallRule struct {
UUID string `gorm:"primaryKey" json:"uuid"`
Family string `json:"family"`
Protocol string `json:"protocol"`
SourceAddress string `json:"sourceAddress"`
SourcePort string `json:"sourcePort"`
DestinationAddress string `json:"destinationAddress"`
DestinationPort string `json:"destinationPort"`
Interface string `json:"interface"`
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
Action string `json:"action"`
Description string `gorm:"type:text" json:"description"`
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
Priority *int `json:"priority,omitempty"`
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
Origin string `json:"origin"`
Owner string `json:"owner"`
Revision uint `gorm:"default:1" json:"revision"`
}
+75 -9
View File
@@ -1,5 +1,7 @@
package model
import "time"
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
@@ -33,14 +35,78 @@ type MonitorNetwork 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
ProductName string `json:"productName"`
GPUUtil float64 `json:"gpuUtil"`
Temperature float64 `json:"temperature"`
PowerDraw float64 `json:"powerDraw"`
MaxPowerLimit float64 `json:"maxPowerLimit"`
MemUsed float64 `json:"memUsed"`
MemTotal float64 `json:"memTotal"`
FanSpeed int `json:"fanSpeed"`
Processes string `json:"processes"`
ProductName string `json:"productName"`
GPUUtil *float64 `json:"gpuUtil"`
Temperature *float64 `json:"temperature"`
PowerDraw *float64 `json:"powerDraw"`
MaxPowerLimit *float64 `json:"maxPowerLimit"`
MemUsed *float64 `json:"memUsed"`
MemTotal *float64 `json:"memTotal"`
FanSpeed *float64 `json:"fanSpeed"`
Processes string `json:"processes"`
}
type MonitorVLLM struct {
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
CreatedAt time.Time `json:"createdAt"`
AppInstallID uint `json:"appInstallID"`
Status string `json:"status"`
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"`
CacheUsage *float64 `json:"cacheUsage"`
PromptThroughput *float64 `json:"promptThroughput"`
GenerationThroughput *float64 `json:"generationThroughput"`
RequestThroughput *float64 `json:"requestThroughput"`
TimeToFirstToken *float64 `json:"timeToFirstToken"`
TimePerOutputToken *float64 `json:"timePerOutputToken"`
RequestLatency *float64 `json:"requestLatency"`
PrefillTime *float64 `json:"prefillTime"`
DecodeTime *float64 `json:"decodeTime"`
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
}
-50
View File
@@ -1,50 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type IDockerPortGuardRepo interface {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
}
type DockerPortGuardRepo struct{}
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", false).
Order("family, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids).
Delete(&model.DockerPortGuardPolicy{}).Error
}
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for i := range policies {
policies[i].ReadOnly = false
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
}).Create(&policies[i]).Error; err != nil {
return err
}
}
return nil
})
}
-187
View File
@@ -1,187 +0,0 @@
package repo
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
)
type IFirewallRuleRepo interface {
Create(context.Context, *model.FirewallRule) error
GetByUUID(context.Context, string) (model.FirewallRule, error)
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
DeleteWithRevision(context.Context, string, uint) error
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
SaveResetOrder(context.Context, []model.FirewallRule) error
}
type FirewallRuleRepo struct {
db *gorm.DB
}
func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{}
}
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
return &FirewallRuleRepo{db: db}
}
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
if err := prepareFirewallRule(rule); err != nil {
return err
}
return r.dbFor(ctx).Create(rule).Error
}
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
var rule model.FirewallRule
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
return rule, err
}
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
var rules []model.FirewallRule
db := r.dbFor(ctx).Model(&model.FirewallRule{})
for _, opt := range opts {
db = opt(db)
}
return rules, db.Find(&rules).Error
}
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
updates = sanitizeRuleUpdates(updates)
updates["revision"] = gorm.Expr("revision + 1")
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrFirewallRuleRevisionConflict
}
return nil
}
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
result := r.dbFor(ctx).
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
Delete(&model.FirewallRule{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrFirewallRuleRevisionConflict
}
return nil
}
func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error {
failures := make(map[string]error)
for start := 0; start < len(rules); start += 500 {
batch := rules[start:min(start+500, len(rules))]
ids := make([][]interface{}, 0, len(batch))
for _, rule := range batch {
ids = append(ids, []interface{}{rule.UUID, rule.Revision})
failures[rule.UUID] = ErrFirewallRuleRevisionConflict
}
var deleted []model.FirewallRule
err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}).
Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error
if err != nil {
for _, rule := range batch {
failures[rule.UUID] = err
}
continue
}
for _, rule := range deleted {
delete(failures, rule.UUID)
}
}
return failures
}
func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error {
if len(rules) == 0 {
return nil
}
return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error {
for _, rule := range rules {
result := tx.Model(&model.FirewallRule{}).
Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision).
Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrFirewallRuleRevisionConflict
}
}
return nil
})
}
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
return firewallDB(ctx, r.db)
}
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
if ctx == nil {
ctx = context.Background()
}
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
return tx.WithContext(ctx)
}
if fallback == nil {
fallback = global.DB
}
return fallback.WithContext(ctx)
}
func prepareFirewallRule(rule *model.FirewallRule) error {
if rule == nil {
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
}
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
}
if rule.UUID == "" {
rule.UUID = uuid.NewString()
}
if rule.Revision == 0 {
rule.Revision = 1
}
if rule.Origin == "" {
rule.Origin = constant.FirewallRuleOriginCreated
}
if rule.Owner == "" {
rule.Owner = constant.FirewallRuleSourceUser
}
return nil
}
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
delete(result, "id")
delete(result, "uuid")
delete(result, "revision")
delete(result, "created_at")
return result
}
-38
View File
@@ -1,38 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
)
type IForwardingRuleRepo interface {
List(context.Context) ([]model.ForwardingRule, error)
CreateBatch(context.Context, []model.ForwardingRule) error
DeleteBatch(context.Context, []uint) error
}
type ForwardingRuleRepo struct{}
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
var rules []model.ForwardingRule
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
return rules, err
}
func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
if len(rules) == 0 {
return nil
}
return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
}
func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
if len(ids) == 0 {
return nil
}
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
}
+103 -13
View File
@@ -1,6 +1,8 @@
package repo
import (
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -10,11 +12,25 @@ import (
type MonitorRepo struct{}
type GPUHistoryPoint struct {
model.MonitorGPU
Bucket int64
PowerPercent *float64
MemoryPercent *float64
ProcessCount *float64
}
type IMonitorRepo interface {
CleanHost() error
CleanGPU() error
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
CountGPU(opts ...DBOption) (int64, error)
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
GetGPUDevices() ([]model.MonitorGPU, error)
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
GetIONames() ([]string, error)
GetNetworkNames() ([]string, error)
CreateMonitorBase(model model.MonitorBase) error
BatchCreateMonitorGPU(list []model.MonitorGPU) error
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
DelMonitorIO(timeForDelete time.Time) error
DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
}
func NewIMonitorRepo() IMonitorRepo {
return &MonitorRepo{}
}
func (s *MonitorRepo) CleanHost() error {
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
return err
}
}
return nil
}
func (s *MonitorRepo) CleanGPU() error {
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
}
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
var data []model.MonitorBase
db := global.MonitorDB
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
err := db.Find(&data).Error
return data, err
}
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
db := global.GPUMonitorDB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&data).Error
return data, err
func (u *MonitorRepo) GetIONames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
}
func (s *MonitorRepo) WithByProductName(name string) DBOption {
return func(g *gorm.DB) *gorm.DB {
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
}
+78
View File
@@ -0,0 +1,78 @@
package repo
import (
"errors"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
HistogramSamples string
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
return global.VLLMMonitorDB.Create(point).Error
}
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
var point model.MonitorVLLM
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
err := db.Order("created_at DESC, id DESC").First(&point).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return point, nil
}
return point, err
}
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
var count int64
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
err := db.Count(&count).Error
return count, err
}
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
var columns []string
if seconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
for _, column := range metrics {
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
continue
}
columns = append(columns, operation+"("+column+") AS "+column)
}
if aggregation != "max" {
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
db = db.Order("created_at ASC, id ASC")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
}
+53
View File
@@ -1,11 +1,13 @@
package repo
import (
"context"
"errors"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type SettingRepo struct{}
@@ -19,12 +21,16 @@ type ISettingRepo interface {
WithByKey(key string) DBOption
UpdateOrCreate(key, value string) error
UpdateValues(map[string]string) error
GetDescription(opts ...DBOption) (model.CommonDescription, error)
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
CreateDescription(data *model.CommonDescription) error
SaveDescriptions(context.Context, []model.CommonDescription) error
UpdateDescription(id string, val map[string]interface{}) error
DelDescription(id string) error
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
WithDescriptionIDs(ids []string) DBOption
WithByDescriptionID(id string) DBOption
}
@@ -90,6 +96,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
return global.DB.Model(&setting).UpdateColumn("value", value).Error
}
func (s *SettingRepo) UpdateValues(values map[string]string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
for key, value := range values {
var setting model.Setting
err := tx.Where("key = ?", key).First(&setting).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
return err
}
} else if err != nil {
return err
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
return err
}
}
return nil
})
}
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
var lists []model.CommonDescription
db := global.DB.Model(&model.CommonDescription{})
@@ -111,14 +136,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
return global.DB.Create(data).Error
}
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"description"}),
}).CreateInBatches(&descriptions, 100).Error
}
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
}
func (s *SettingRepo) DelDescription(id string) error {
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
}
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
var deleted int64
for start := 0; start < len(ids); start += 500 {
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
if emptyOnly {
query = query.Where("description = ? AND is_pinned = ?", "", false)
}
result := query.Delete(&model.CommonDescription{})
deleted += result.RowsAffected
if result.Error != nil {
return deleted, result.Error
}
}
return deleted, nil
}
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("id = ?", id)
}
}
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
}
+102 -117
View File
@@ -4,11 +4,8 @@ import (
"encoding/json"
"fmt"
"mime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -17,14 +14,13 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"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"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
"github.com/shirou/gopsutil/v4/disk"
)
type AlertService struct{}
@@ -109,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
return 0, nil, err
}
cronjobProjects := make(map[string]uint)
var cronjobIDs []uint
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{
ID: item.ID,
@@ -119,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
Method: item.Method,
Title: item.Title,
Project: item.Project,
TaskName: taskName,
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
@@ -190,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
return err
}
} 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
if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
@@ -207,11 +245,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
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)
if err != nil {
return err
}
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 {
return err
}
@@ -278,6 +329,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
return err
}
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 {
return err
}
@@ -299,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
}
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
var disks []dto.DiskDTO
excludes := map[string]struct{}{
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
infos := loadDiskInfo(true)
disks := make([]dto.DiskDTO, 0, len(infos))
for _, item := range infos {
disks = append(disks, dto.DiskDTO(item))
}
stdout, err := executeDiskCommand()
if err != nil {
return disks, nil
}
lines := strings.Split(stdout, "\n")
var mounts []dto.AlertDiskInfo
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
mountPoint := strings.Join(fields[6:], " ")
if shouldExclude(fields, mountPoint, excludes) {
continue
}
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
}
var (
wg sync.WaitGroup
mu sync.Mutex
)
wg.Add(len(mounts))
for i := 0; i < len(mounts); i++ {
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
defer wg.Done()
var itemData dto.DiskDTO
itemData.Path = mount.Mount
itemData.Type = mount.Type
itemData.Device = mount.Device
select {
case <-timeoutCh:
mu.Lock()
disks = append(disks, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
default:
state, err := disk.Usage(mount.Mount)
if err != nil {
mu.Lock()
disks = append(disks, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
return
}
itemData.Total = state.Total
itemData.Free = state.Free
itemData.Used = state.Used
itemData.UsedPercent = state.UsedPercent
itemData.InodesTotal = state.InodesTotal
itemData.InodesUsed = state.InodesUsed
itemData.InodesFree = state.InodesFree
itemData.InodesUsedPercent = state.InodesUsedPercent
mu.Lock()
disks = append(disks, itemData)
mu.Unlock()
}
}(time.After(5*time.Second), mounts[i])
}
wg.Wait()
sort.Slice(disks, func(i, j int) bool {
return disks[i].Path < disks[j].Path
})
return disks, nil
}
func executeDiskCommand() (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
if err != nil {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
}
if err != nil {
return stdout, err
}
var lines []string
for _, line := range strings.Split(stdout, "\n") {
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
continue
}
lines = append(lines, line)
}
if len(lines) == 0 {
return "", nil
}
return strings.Join(lines, "\n"), nil
}
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
return true
}
if strings.TrimSpace(fields[1]) == "tmpfs" {
return true
}
if strings.Contains(fields[2], "K") {
return true
}
if strings.Contains(mountPoint, "docker") {
return true
}
_, excluded := excludes[mountPoint]
return excluded
}
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
var (
opts []repo.DBOption
@@ -1021,6 +967,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
alertRepo.WithByType(updateAlert.Type),
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 {
shouldUpdate := false
@@ -1034,6 +997,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
shouldUpdate = true
}
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
shouldUpdate = true
}
if shouldUpdate {
if err := alertRepo.Update(
@@ -1055,3 +1021,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
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
}
+25 -21
View File
@@ -530,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
if err != nil {
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(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
)
isAlert := count >= int(alert.Count)
if isAlert {
@@ -570,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
}
}
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
whitelist = append(whitelist, "127.0.0.0/8", "::1")
for _, addr := range interfaceAddrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
continue
}
whitelist = append(whitelist, ipNet.IP.String())
}
return whitelist
}
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
filtered := make([]model.LoginLog, 0, len(records))
for _, record := range records {
@@ -1049,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
global.LOG.Errorf("error getting disk list, err: %v", err)
return err
}
var errMsgs []string
for _, item := range diskList {
err := checkAndCreateDiskAlert(alert, item.Path)
if err != nil {
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
errMsgs = append(errMsgs, errMsg)
global.LOG.Errorf("%s", errMsg)
if item.Total == 0 {
continue
}
}
if len(errMsgs) > 0 {
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
}
return nil
}
func processSingleDisk(alert dto.AlertDTO) error {
err := checkAndCreateDiskAlert(alert, alert.Project)
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
if err != nil {
global.LOG.Errorf("%s", err.Error())
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
return err
}
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
return nil
}
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
usageStat, err := psutil.DISK.GetUsage(path, false)
if err != nil {
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
return err
}
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
commonTotal := float64(alert.Count)
if alert.Cycle == 1 {
commonTotal *= 1024 * 1024 * 1024
}
if usedTotal < commonTotal {
return nil
return
}
params := createAlertDiskParams(path, usedStr)
sender := NewAlertSender(alert, alert.Project)
sender.ResourceSend(path, params)
return nil
}
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
+92 -26
View File
@@ -16,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"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/jinzhu/copier"
"github.com/pkg/errors"
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
EntryID: cronjob.ID,
}
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 {
item.AlertCount = alertInfo.SendCount
} else {
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
AlertType: cronjob.Type,
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
if alertInfo.SendCount != 0 {
item.AlertTitle = alertInfo.Title
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
} else {
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)
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
if alertInfo.SendCount != 0 {
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
} else {
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 {
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 {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 {
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
} else {
cronjob.Status = constant.StatusDisable
}
_ = cronjobRepo.Create(&cronjob)
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
if item.AlertTitle != "" && item.AlertMethod != "" {
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
status := constant.AlertEnable
if item.AlertCount == 0 {
status = constant.AlertDisable
}
createAlert := dto.AlertCreate{
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: status,
AdvancedParams: advanced,
}
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
return err
}
_ = NewIAlertService().CreateAlert(createAlert, operator)
}
}
return nil
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) 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))
if cronjob.ID != 0 {
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 != "" {
createAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
AdvancedParams: advanced,
}
err := NewIAlertService().CreateAlert(createAlert, operator)
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 {
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
if err != nil {
return err
}
var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != 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 {
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{})
cronjob.EntryIDs = cronModel.EntryIDs
cronjob.Type = cronModel.Type
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return err
}
updateAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
AdvancedParams: advanced,
}
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil {
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
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 {
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
if cronjob.ID == 0 {
+1
View File
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
return nil
}, nil)
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
}
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
+35 -6
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -23,6 +24,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"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/files"
"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)
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
}()
return
}
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
go func() {
if err := taskItem.Execute(); err != nil {
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(taskItem.ID) == 0 {
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(storedTask.ID) == 0 {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
} else {
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"
}
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{
Result: result,
TaskName: cronjob.Name,
AlertType: cronjob.Type,
EntryID: cronjob.ID,
+89 -57
View File
@@ -242,7 +242,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
currentInfo.SwapMemoryUsed = swapInfo.Used
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
currentInfo.DiskData = loadDiskInfo()
currentInfo.DiskData = loadDiskInfo(false)
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
if ioOption == "all" {
@@ -455,26 +455,9 @@ type diskInfo struct {
Device string
}
func loadDiskInfo() []dto.DiskInfo {
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
var datas []dto.DiskInfo
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
stdout, err := cmdMgr.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil {
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil {
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
return datas
}
}
stdout := loadDiskMounts()
lines := strings.Split(stdout, "\n")
var mounts []diskInfo
@@ -522,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
itemData.Type = mount.Type
itemData.Device = mount.Device
type diskResult struct {
state *disk.UsageStat
err error
}
resultCh := make(chan diskResult, 1)
go func() {
state, err := psutil.DISK.GetUsage(mount.Mount, false)
resultCh <- diskResult{state: state, err: err}
}()
select {
case <-time.After(5 * time.Second):
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
case result := <-resultCh:
if result.err != nil {
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
return
}
itemData.Total = result.state.Total
itemData.Free = result.state.Free
itemData.Used = result.state.Used
itemData.UsedPercent = result.state.UsedPercent
itemData.InodesTotal = result.state.InodesTotal
itemData.InodesUsed = result.state.InodesUsed
itemData.InodesFree = result.state.InodesFree
itemData.InodesUsedPercent = result.state.InodesUsedPercent
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
if err != nil {
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
} else {
itemData.Total = state.Total
itemData.Free = state.Free
itemData.Used = state.Used
itemData.UsedPercent = state.UsedPercent
itemData.InodesTotal = state.InodesTotal
itemData.InodesUsed = state.InodesUsed
itemData.InodesFree = state.InodesFree
itemData.InodesUsedPercent = state.InodesUsedPercent
}
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
}(mounts[i])
}
wg.Wait()
@@ -569,6 +531,69 @@ func loadDiskInfo() []dto.DiskInfo {
return datas
}
var diskMountsMu sync.Mutex
func loadDiskMounts() string {
if !diskMountsMu.TryLock() {
return ""
}
resultCh := make(chan string, 1)
go func() {
var stdout string
defer func() {
diskMountsMu.Unlock()
resultCh <- stdout
}()
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
output, err := cmdMgr.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil {
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
output, err = cmdMgr2.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil {
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
return
}
}
stdout = output
}()
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
select {
case stdout := <-resultCh:
return stdout
case <-timer.C:
global.LOG.Error("load disk mounts timed out; df collection is still running")
return ""
}
}
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
type diskResult struct {
state *disk.UsageStat
err error
}
resultCh := make(chan diskResult, 1)
go func() {
state, err := psutil.DISK.GetUsage(path, forceRefresh)
resultCh <- diskResult{state: state, err: err}
}()
select {
case <-time.After(5 * time.Second):
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
case result := <-resultCh:
return result.state, result.err
}
}
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
ok, client := accelerator.New()
if !ok {
@@ -588,6 +613,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
xpuData []dto.XPUInfo
)
for _, device := range snapshot.Devices {
if device.ParentID != "" {
continue
}
switch device.Kind {
case accelerator.KindGPU:
if device.GPU == nil {
@@ -597,7 +625,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
if err := copier.Copy(&dataItem, device.GPU); err != nil {
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
gpuData = append(gpuData, dataItem)
case accelerator.KindNPU:
+1 -1
View File
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
if baseInfo.SwapMemoryTotal != 0 {
baseInfo.SwapDetails = loadSwap()
}
disks := loadDiskInfo()
disks := loadDiskInfo(false)
for _, item := range disks {
baseInfo.MaxSize += item.Free
}
+3
View File
@@ -487,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
if err := controller.Handle(req.Operation, service); err != nil {
return err
}
if req.Operation == "start" || req.Operation == "restart" {
return RestoreDockerPortGuard(context.Background())
}
return nil
}
+7 -7
View File
@@ -32,14 +32,14 @@ var (
scriptRepo = repo.NewIScriptRepo()
cronjobRepo = repo.NewICronjobRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
settingRepo = repo.NewISettingRepo()
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
backupRepo = repo.NewIBackupRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()
websiteRepo = repo.NewIWebsiteRepo()
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
+1 -1
View File
@@ -1310,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
}
func (f *FileService) GetHostMount() []dto.DiskInfo {
return loadDiskInfo()
return loadDiskInfo(false)
}
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
File diff suppressed because it is too large Load Diff
+233 -157
View File
@@ -2,17 +2,16 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"slices"
"sort"
"strconv"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
@@ -39,9 +38,8 @@ const (
)
type DockerPortGuardService struct {
policies repo.IDockerPortGuardRepo
runtime dockerfirewall.Runtime
runtimeForBackend func(string) dockerfirewall.Runtime
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
client func() (*client.Client, error)
version func(string) string
}
@@ -50,24 +48,35 @@ var dockerPortGuardServiceMu sync.Mutex
type IDockerPortGuardService interface {
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
Operate(context.Context, dto.DockerPortGuardOperation) error
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
Reconcile(context.Context) error
Restore(context.Context) error
}
func NewIDockerPortGuardService() IDockerPortGuardService {
return newDockerPortGuardService()
}
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
policies, err := s.policies.ListManaged(ctx)
families, err := loadFirewallFamilies()
if err != nil {
return dto.DockerPortGuardList{}, err
}
backend := selectedDockerFirewallBackend("")
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
if err != nil {
return dto.DockerPortGuardList{}, err
}
policies := dockerGuardInventoryEndpoints(inventory)
unavailable := func() dto.DockerPortGuardList {
backend := selectedDockerFirewallBackend("")
base := s.runtimeStatus(s.guardRuntime(backend), backend)
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
base.Message = i18n.Get("ErrDockerFailed")
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
}
cli, err := s.client()
if err != nil {
@@ -79,13 +88,13 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
return unavailable(), nil
}
detectedBackend := dockerFirewallBackend(info)
backend := selectedDockerFirewallBackend(detectedBackend)
base := s.runtimeStatus(s.guardRuntime(backend), backend)
backend = selectedDockerFirewallBackend(detectedBackend)
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil {
return dto.DockerPortGuardList{}, err
}
annotateDockerEndpointManagement(endpoints, detectedBackend)
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
sort.Slice(endpoints, func(i, j int) bool {
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
@@ -96,6 +105,24 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
}
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
backend := string(provider)
if backend == "" {
backend = selectedDockerFirewallBackend("")
}
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
}
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
if err != nil {
return dto.FirewallSubsystemBackup{}, err
}
if inventory.Policies == nil {
inventory.Policies = []dockerfirewall.Policy{}
}
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
}
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
cli, err := s.client()
if err != nil {
@@ -117,7 +144,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
if info, infoErr := cli.Info(ctx); infoErr == nil {
backend = dockerFirewallBackend(info)
}
annotateDockerEndpointManagement(endpoints, backend)
annotateDockerEndpointManagement(ctx, endpoints, backend)
return groupDockerGuardContainers(endpoints), nil
}
@@ -126,34 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
defer dockerPortGuardServiceMu.Unlock()
switch request.Operation {
case "initialize":
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
policies, err := s.runtimePolicies(ctx)
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if err := runtime.Initialize(policies, inventory); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
return err
}
return nil
return s.initialize(ctx, request, nil)
case "bind":
runtime, _, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
if err := runtime.Bind(); err != nil {
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
@@ -162,9 +172,9 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
if s.runtime != nil {
err = s.runtime.Unbind()
} else {
err = errors.Join(dockerfirewall.NewIptables().Unbind(), dockerfirewall.NewNftables().Unbind())
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
}
if err != nil {
if err = errors.Join(err, ctx.Err()); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
@@ -174,55 +184,22 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
}
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
if request.Operation != "initialize" {
return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
if request.Operation != "initialize" {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
}
var runtime dockerfirewall.Runtime
var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error {
var err error
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
if err != nil {
return err
if request.BackupFile != "" {
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
return dto.FilterChainOperationResponse{}, err
}
t.Logf("backend=%s", backend)
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
policies, err := s.runtimePolicies(t.TaskCtx)
if err != nil {
return err
}
t.Logf("backend=%s", backend)
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
return runtime.Initialize(policies, inventory)
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error {
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
return err
}
return nil
}, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
return s.initialize(t.TaskCtx, request, t)
})
}
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
@@ -230,20 +207,34 @@ func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolic
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
labels := make([]string, len(uuids))
for i, id := range uuids {
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
ctx := t.TaskCtx
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil {
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
return s.reconcileLocked(ctx)
wanted := make(map[string]bool, len(uuids))
for _, id := range uuids {
wanted[id] = true
}
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
for _, policy := range inventory.Policies {
if wanted[policy.UUID] {
delete(wanted, policy.UUID)
} else {
remaining = append(remaining, policy)
}
}
if len(wanted) > 0 {
return filter.ErrRuleStale
}
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
})
}
@@ -252,7 +243,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
labels := make([]string, len(request.Policies))
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
count := 0
for i, policy := range request.Policies {
@@ -264,7 +255,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
}
if normalized.Mode == dockerfirewall.ModeAll {
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
count++
} else {
count += len(normalized.Sources)
@@ -275,42 +266,142 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
if count > filter.MaxAtomicExpansion {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
encoded, err := json.Marshal(normalized.Sources)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
}
policies = append(policies, model.DockerPortGuardPolicy{
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
})
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
Family: normalized.Family, HostIP: normalized.HostIP, HostPort: normalized.HostPort, Protocol: normalized.Protocol,
})
normalized.UUID = uuid.NewString()
policies = append(policies, normalized)
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error {
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
ctx := t.TaskCtx
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
if request.Import {
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
merged, err := mergeDockerBackup(inventory, backup, backend, t)
if err != nil {
return err
}
current = merged.Policies
}
byEndpoint := make(map[string]int, len(current))
for i, policy := range current {
byEndpoint[dockerPolicyEndpointKey(policy)] = i
}
for i := range policies {
key := dockerPolicyEndpointKey(policies[i])
index, exists := byEndpoint[key]
if request.Import {
if !exists || current[index].UUID != policies[i].UUID {
labels[i] = ""
continue
}
} else if exists {
policies[i].UUID = current[index].UUID
current[index] = policies[i]
} else {
byEndpoint[key] = len(current)
current = append(current, policies[i])
}
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
})
}
if len(endpoints) == 0 {
return nil
}
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
return err
}
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
return err
}
return s.reconcileLocked(ctx)
return nil
})
}
func NewIDockerPortGuardService() IDockerPortGuardService {
return newDockerPortGuardService()
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
enabled, err := dockerPortGuardPersistedEnabled()
if err != nil || !enabled {
return err
}
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
before := len(backup.Docker.Policies)
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
}
missing := make(map[string]bool)
needsBind := false
for _, family := range families {
status := runtime.Status(family)
if status.Reason == dockerfirewall.ReasonInspectFailed {
return fmt.Errorf("inspect Docker guard %s failed", family)
}
if status.Reason == dockerfirewall.ReasonCommandMissing {
continue
}
missing[family] = !status.Initialized
needsBind = needsBind || (status.Initialized && !status.Effective)
}
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
if needsBind {
return runtime.Bind(families...)
}
return nil
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
for _, policy := range inventory.Policies {
missing[policy.Family] = false
}
if inventory.RuleOrders == nil {
inventory.RuleOrders = make(map[string][]int64)
}
for _, policy := range backup.Docker.Policies {
if !missing[policy.Family] {
continue
}
inventory.Policies = append(inventory.Policies, policy)
key := policy.Family + "\x00" + policy.UUID
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
}
return runtime.Initialize(inventory.Policies, inventory, families...)
}
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string) dto.DockerPortGuardBase {
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
ipv6 := runtime.Status(dockerfirewall.FamilyIPv6)
var ipv6 dockerfirewall.FamilyStatus
if ipv6Enabled {
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
}
version := "-"
if s.version != nil {
version = s.version(backend)
@@ -320,83 +411,68 @@ func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, b
name = "nftables-docker"
}
return dto.DockerPortGuardBase{
IPv6Enabled: ipv6Enabled,
Name: name,
Version: version,
Backend: backend,
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || ipv6.Reason != dockerfirewall.ReasonCommandMissing,
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
Initialized: ipv4.Initialized || ipv6.Initialized,
Bound: ipv4.Bound || ipv6.Bound,
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
}
}
func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint {
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies))
for _, policy := range policies {
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources,
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
})
}
return endpoints
}
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []model.DockerPortGuardPolicy, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies))
for _, policy := range policies {
byEndpoint[fmt.Sprintf("%s|%s|%d|%s", policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
matched := make(map[int]bool, len(policies))
byEndpoint := make(map[string]int, len(policies))
for index, policy := range policies {
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
if _, exists := byEndpoint[key]; !exists {
byEndpoint[key] = index
}
}
for i := range endpoints {
key := fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol)
policy, ok := byEndpoint[key]
if !ok {
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
index, exists := byEndpoint[key]
if !exists {
continue
}
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, sources
endpoints[i].Description = policy.Description
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
delete(byEndpoint, key)
policy := policies[index]
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
matched[index] = true
}
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
for _, policy := range byEndpoint {
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
})
orphans := make([]dto.DockerPortGuardEndpoint, 0)
for i, policy := range policies {
if !matched[i] {
orphans = append(orphans, policy)
}
}
return endpoints, orphanPolicies
return endpoints, orphans
}
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
if s.client == nil || len(requested) == 0 {
return nil
return ctx.Err()
}
cli, err := s.client()
if err != nil {
return nil
return ctx.Err()
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return nil
return ctx.Err()
}
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil {
return nil
return ctx.Err()
}
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
if err := ctx.Err(); err != nil {
return err
}
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
targets := make(map[string]string, len(endpoints))
for _, endpoint := range endpoints {
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
+333 -129
View File
@@ -10,7 +10,7 @@ import (
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -18,7 +18,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm"
)
@@ -28,106 +31,115 @@ type IFirewallSettingService interface {
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
}
type FirewallSettingService struct{}
var firewallWhitelistMu sync.Mutex
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
firewallWhitelistMu.Lock()
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
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
if result == nil {
result = newFirewallService().SyncPortWhitelist(ctx)
}
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
})
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
current = append(current, request.Rule)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
firewallWhitelistMu.Lock()
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
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
if result == nil {
result = newFirewallService().SyncPortWhitelist(ctx)
}
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return err
}
current[index] = 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
})
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return err
}
current[index] = request.Rule
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
if request.Rule == nil {
return fmt.Errorf("select one firewall port whitelist rule to delete")
}
firewallWhitelistMu.Lock()
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.Rule)
if err != nil {
return err
}
current = slices.Delete(current, index, index+1)
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
})
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
if request.Rule == nil {
return filter.ErrInvalidRule
}
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return err
}
current = slices.Delete(current, index, index+1)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
families, err := loadFirewallFamilies()
if err != nil {
return dto.FirewallSettings{}, err
}
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() {
@@ -153,7 +165,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
if err != nil {
option.Message = err.Error()
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
overview, err := loadSystemFirewallOverview(name, "base")
overview, err := loadSystemFirewallOverview(name, "base", families)
if err != nil {
option.Message = err.Error()
}
@@ -180,18 +192,18 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.Forwarding.Selected {
manager, err := newForwardingAdapterFor(name)
manager, err := newForwardingAdapterFor(ctx, name)
if err != nil {
option.Message = err.Error()
} else {
status, statusErr := loadForwardingFirewallOverview(manager)
status, statusErr := loadForwardingFirewallOverview(manager, families)
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
if statusErr != nil {
option.Message = statusErr.Error()
} else {
option.Initialized, option.Bound = status.IsInit, status.IsBind
}
if name == constant.FirewallProviderIptables && !option.IPv6.Available {
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
}
@@ -228,20 +240,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
option.Active = false
}
if option.Active {
guard := newDockerFirewallRuntime(name)
ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
option.Initialized = ipv4.Initialized || ipv6.Initialized
option.Bound = ipv4.Bound || ipv6.Bound
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
guard := newDockerFirewallRuntime(ctx, name)
for _, family := range families {
status := guard.Status(family)
option.Initialized = option.Initialized || status.Initialized
option.Bound = option.Bound || status.Bound
info := dto.FirewallBackendFamilyStatus{
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
}
if family == constant.FirewallFamilyIPv4 {
option.IPv4 = info
} else {
option.IPv6 = info
}
}
}
result.Docker.Options = append(result.Docker.Options, option)
}
var err error
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
result.PortWhitelist, err = loadPortWhitelistSetting()
if err != nil {
return result, err
}
@@ -256,6 +273,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if request.Operation == "cleanup" {
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
return err
}
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
@@ -273,13 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
}
if request.Operation == "initialize" {
service := newFirewallService()
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
whitelistErr := service.SyncPortWhitelist(ctx)
return errors.Join(rulesErr, whitelistErr)
return whitelistErr
}
return nil
case "forwarding":
return s.operateForwarding(request)
return s.operateForwarding(ctx, request)
case "docker":
return s.operateDocker(ctx, request)
default:
@@ -287,6 +307,211 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
}
}
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
}
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
}
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
}
subsystem := ""
switch request.Subsystem {
case "system":
subsystem = firewallTaskHost
case "forwarding":
subsystem = firewallTaskForwarding
case "docker":
subsystem = firewallTaskDocker
default:
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if !slices.Contains(families, request.Family) {
return fmt.Errorf("IPv6 firewall support is disabled")
}
initialize := request.Operation != "bind"
switch request.Subsystem {
case "system":
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
return err
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return err
}
firewallRuleMutationMu.Lock()
if request.Backend == constant.FirewallProviderIptables {
err = iptables_helper.OperateFamily(request.Family, initialize, required)
} else {
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
}
firewallRuleMutationMu.Unlock()
if err != nil {
return err
}
if initialize {
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
return err
}
}
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
case "forwarding":
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := newForwardingAdapter(t.TaskCtx)
if err != nil {
return err
}
if manager.Name() != request.Backend {
return filter.ErrProviderUnavailable
}
if err := manager.OperateFamily(request.Family, initialize); err != nil {
return err
}
rules, err := manager.List()
if err != nil {
return err
}
if err := persistForwardingRules(manager, rules); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
default:
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
if err != nil {
return err
}
if backend != request.Backend {
return filter.ErrProviderUnavailable
}
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if err := persistDockerRules(backend, inventory); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
}
})
}
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if request.Status == constant.StatusEnable {
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
value, err := json.Marshal(ipv4PortWhitelist(ports))
if err != nil {
return err
}
installed := lifecycle.InstalledProviders()
for _, selection := range []struct{ subsystem, key string }{
{"system", constant.FirewallSystemBackendKey},
{"forwarding", constant.FirewallForwardingBackendKey},
{"docker", constant.FirewallDockerBackendKey},
} {
backend, err := settingRepo.GetValueByKey(selection.key)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
backend = strings.ToLower(strings.TrimSpace(backend))
if backend == "" {
if selection.subsystem == "system" {
if len(installed) == 0 {
continue
}
client, err := NewSelectedSystemFirewallClient()
if err != nil {
return err
}
backend = client.Name()
} else {
backend = constant.FirewallProviderIptables
}
}
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
continue
}
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
switch selection.subsystem {
case "system":
if backend == constant.FirewallProviderIptables {
err = iptables_helper.UnbindIPv6BaseChains()
} else {
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
if err == nil {
err = nftables_helper.PersistRuleset(t.TaskCtx)
}
}
case "forwarding":
var manager forwarding.Adapter
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
if err == nil {
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
}
case "docker":
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
}
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
return err
}
}
if err := t.TaskCtx.Err(); err != nil {
return err
}
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
})
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
@@ -297,9 +522,6 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
if _, err := lifecycle.NewClient(request.Backend); err != nil {
return err
}
if request.Operation == "cleanup" {
return cleanupSystemBackend(request.Backend)
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
if previous == "" {
if client, err := lifecycle.NewClient(""); err == nil {
@@ -363,32 +585,21 @@ func systemFirewallBackendInitialized(backend string) (bool, error) {
return client.Status()
}
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
manager, err := newForwardingAdapterFor(request.Backend)
if err != nil {
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
return err
}
if request.Operation == "cleanup" {
if err := manager.Cleanup(); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
return err
}
recordForwardingSyncError(nil)
return nil
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
detected, err := newForwardingAdapter()
detected, err := newForwardingAdapter(ctx)
if err != nil {
return err
}
current = detected.Name()
}
initialized, err := forwardingBackendInitialized(current)
initialized, err := forwardingBackendInitialized(ctx, current)
if err != nil {
return err
}
@@ -400,14 +611,13 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
return err
}
if request.Operation == "initialize" {
return newForwardingService().Enable()
return newForwardingService().Enable(ctx)
}
recordForwardingSyncError(nil)
return nil
}
func forwardingBackendInitialized(backend string) (bool, error) {
manager, err := newForwardingAdapterFor(backend)
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
manager, err := newForwardingAdapterFor(ctx, backend)
if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil
@@ -427,13 +637,7 @@ func forwardingBackendInitialized(backend string) (bool, error) {
}
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
guard := newDockerFirewallRuntime(request.Backend)
if request.Operation == "cleanup" {
if err := guard.Cleanup(); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" {
current := previous
@@ -443,7 +647,7 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
current = constant.FirewallProviderIptables
}
}
initialized, err := dockerGuardBackendInitialized(current)
initialized, err := dockerGuardBackendInitialized(ctx, current)
if err != nil {
return err
}
@@ -469,8 +673,8 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
return nil
}
func dockerGuardBackendInitialized(backend string) (bool, error) {
guard := newDockerFirewallRuntime(backend)
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
guard := newDockerFirewallRuntime(ctx, backend)
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
-396
View File
@@ -1,396 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
)
var (
firewallRuleSyncTaskMu sync.Mutex
firewallRuleSyncTaskID string
)
type firewallDatabaseSyncAdapter interface {
previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
}
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
provider, err := s.selectedProvider(ctx)
if err != nil {
return err
}
if _, err := s.syncPortWhitelist(ctx, provider, ports); err != nil {
return err
}
return s.removeTransferredSystemPortRules(ctx, provider, ports)
}
func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
switch strings.TrimSpace(request.Subsystem) {
case "forwarding":
service := s.forwardingSync
if service == nil {
service = newForwardingService()
}
return service.previewRuleSync(ctx, request)
case "docker":
service := s.dockerSync
if service == nil {
service = newDockerPortGuardService()
}
return service.previewRuleSync(ctx, request)
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
_, rules, _, err := s.loadFirewallSyncRules(ctx, request)
preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))}
for _, rule := range rules {
preview.Add(rule.FirewallRuleSyncItem)
}
return preview, err
}
func (s *FirewallService) SyncRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
switch strings.TrimSpace(request.Subsystem) {
case "forwarding":
service := s.forwardingSync
if service == nil {
service = newForwardingService()
}
return service.syncRules(ctx, request)
case "docker":
service := s.dockerSync
if service == nil {
service = newDockerPortGuardService()
}
return service.syncRules(ctx, request)
default:
return s.syncSystemRules(ctx, clientIP, request)
}
}
func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) {
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()
return currentFirewallRuleSyncTaskLocked()
}
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
return s.reconcileLocked(ctx)
}
func (s *ForwardingService) Restore(ctx context.Context) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
enabled, err := s.forwardingEnabled()
if err != nil || !enabled {
if err != nil {
recordForwardingSyncError(err)
}
return err
}
manager, err := s.clientFactory()
if err != nil {
recordForwardingSyncError(err)
return err
}
stored, err := s.rules.List(ctx)
if err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.ReplaceRules(forwardingRulesFromModels(stored))
recordForwardingSyncError(err)
return err
}
func (s *ForwardingService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider)
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil
}
func (s *ForwardingService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider)
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules)
desired := make([]forwarding.Rule, 0, len(candidates))
for _, candidate := range candidates {
if candidate.err == nil {
desired = append(desired, candidate.rule)
}
}
if preview.Blocked > 0 {
return firewallSyncResult(preview, nil, false), nil
}
if len(desired) == 0 && !targetInitialized {
return firewallSyncResult(preview, nil, true), nil
}
reconcileErr := func() error {
if len(desired) > 0 {
if err := s.persistForwardingEnabled(); err != nil {
return err
}
if err := s.initializeForwarding(target); err != nil {
return err
}
}
if err := target.ReplaceRules(desired); err != nil {
return err
}
return verifyForwardingRuleSync(target, desired)
}()
result := firewallSyncResult(preview, reconcileErr, true)
recordForwardingSyncError(reconcileErr)
if reconcileErr != nil {
if preview.Ready == 0 {
return result, reconcileErr
}
return result, nil
}
return result, nil
}
func (s *DockerPortGuardService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request)
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
targetInventory, err := runtime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil
}
func (s *DockerPortGuardService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request)
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
runtimePolicies := make([]dockerfirewall.Policy, 0, len(policies))
for _, policy := range policies {
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
runtimePolicies = append(runtimePolicies, dockerfirewall.Policy{
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources,
})
}
targetInventory, err := targetRuntime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory)
for _, item := range preview.Items {
if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
return firewallSyncResult(preview, nil, false), nil
}
}
if preview.Ready == 0 && preview.Removed == 0 {
return firewallSyncResult(preview, nil, false), nil
}
reconcileErr := func() error {
if err := reconcileDockerFirewall(target, runtimePolicies, targetRuntime, targetInventory); err != nil {
return err
}
if err := verifyDockerFirewall(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
return err
}
if len(policies) == 0 {
return nil
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
}()
result := firewallSyncResult(preview, reconcileErr, true)
if reconcileErr != nil {
return result, reconcileErr
}
return result, nil
}
func (s *FirewallService) syncSystemRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
subsystem := strings.TrimSpace(request.Subsystem)
if subsystem == "" {
subsystem = "system"
}
if err := lockFirewallLifecycleIdle(); err != nil {
return dto.FirewallRuleSyncResult{}, err
}
defer firewallLifecycleTaskMu.Unlock()
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()
running, err := currentFirewallRuleSyncTaskLocked()
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
if running.Executing {
return dto.FirewallRuleSyncResult{
Subsystem: subsystem,
TargetProvider: request.TargetProvider,
TaskID: running.TaskID,
Queued: true,
}, nil
}
if subsystem != "system" {
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err)
}
taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error {
result, err := s.syncRules(t.TaskCtx, clientIP, request, t)
if err != nil {
return err
}
if result.Failed > 0 {
return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed}))
}
return nil
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err)
}
firewallRuleSyncTaskID = taskItem.TaskID
go func() {
defer func() {
firewallRuleSyncTaskMu.Lock()
if firewallRuleSyncTaskID == taskItem.TaskID {
firewallRuleSyncTaskID = ""
}
firewallRuleSyncTaskMu.Unlock()
}()
if err := taskItem.Execute(); err != nil && global.LOG != nil {
global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err)
}
}()
return dto.FirewallRuleSyncResult{
Subsystem: "system",
TargetProvider: request.TargetProvider,
TaskID: taskItem.TaskID,
Queued: true,
}, nil
}
func verifyForwardingRuleSync(target forwarding.Adapter, desired []forwarding.Rule) error {
actual, err := target.List()
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
actual, err = normalizeForwardingRuntimeRules(actual)
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
if !firewallRuleStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
}
return nil
}
func reconcileDockerFirewall(backend string, policies []dockerfirewall.Policy, runtime dockerfirewall.Runtime, inventory dockerfirewall.PolicyInventory) error {
families := make(map[string]struct{}, len(policies))
needsInitialize, needsBind := false, false
for _, policy := range policies {
families[policy.Family] = struct{}{}
}
if len(families) == 0 {
initialized := false
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
status := runtime.Status(family)
if status.Reason == dockerfirewall.ReasonInspectFailed {
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
}
initialized = initialized || status.Initialized
}
if initialized {
return runtime.ReplacePolicies(nil, inventory)
}
return nil
}
for family := range families {
status := runtime.Status(family)
needsInitialize = needsInitialize || !status.Initialized
needsBind = needsBind || !status.Bound || !status.Effective
}
var err error
if needsInitialize {
err = runtime.Initialize(policies, inventory)
} else {
if needsBind {
err = runtime.Bind()
}
if err == nil {
err = runtime.ReplacePolicies(policies, inventory)
}
}
if err != nil {
return err
}
for family := range families {
if !runtime.Status(family).Effective {
return fmt.Errorf("Docker firewall target %s is not effective for %s", backend, family)
}
}
return nil
}
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
if err := ReconcileDockerPortGuard(ctx); err != nil {
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
}
}
File diff suppressed because it is too large Load Diff
+155 -388
View File
@@ -5,19 +5,16 @@ import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/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/filter"
@@ -25,51 +22,41 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
const (
forwardingSyncConverged = "converged"
forwardingSyncMissing = "missing"
forwardingSyncRuntimeOnly = "runtime_only"
)
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error)
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
Enable() error
Enable(ctx context.Context) error
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
Restore(context.Context) error
}
type ForwardingService struct {
clientFactory func() (forwarding.Adapter, error)
rules repo.IForwardingRuleRepo
enabled func() (bool, error)
persistBackend func(string) error
markEnabled func() error
clientFactory func(context.Context) (forwarding.Adapter, error)
}
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
var forwardingMutationMu sync.Mutex
var (
forwardingSyncStateMu sync.RWMutex
forwardingLastSyncErr error
)
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) {
families, err := loadFirewallFamilies()
if err != nil {
return dto.FirewallSubsystemStatus{}, err
}
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
selected = constant.FirewallProviderIptables
}
baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(),
Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1,
}
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
baseInfo.Name += "-forward"
}
manager, err := s.clientFactory()
manager, err := s.clientFactory(ctx)
if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled
@@ -82,7 +69,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
return baseInfo, err
}
version, versionErr := client.Version()
status, statusErr := loadForwardingFirewallOverview(manager)
status, statusErr := loadForwardingFirewallOverview(manager, families)
if err := errors.Join(versionErr, statusErr); err != nil {
return baseInfo, err
}
@@ -102,7 +89,10 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
{"iptables", &baseInfo.IPv4},
{"ip6tables", &baseInfo.IPv6},
} {
policy, err := loadForwardPolicy(family.command)
if family.command == "ip6tables" && !baseInfo.IPv6Enabled {
continue
}
policy, err := loadForwardPolicy(ctx, family.command)
if err != nil {
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
continue
@@ -112,66 +102,48 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
return baseInfo, nil
}
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
if request.Strategy != "" {
return 0, nil, nil
func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
var manager forwarding.Adapter
var err error
if provider == "" {
manager, err = s.clientFactory(ctx)
} else {
manager, err = newForwardingAdapterFor(ctx, string(provider))
}
stored, err := s.rules.List(context.Background())
if err != nil {
return dto.FirewallSubsystemBackup{}, err
}
rules, err := manager.List()
if err != nil {
return dto.FirewallSubsystemBackup{}, err
}
return forwardingBackup(manager, rules)
}
func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
manager, err := s.clientFactory(ctx)
if err != nil {
return 0, nil, err
}
manager, err := s.clientFactory()
if err != nil {
return 0, nil, err
}
runtime, err := manager.List()
if err != nil {
return 0, nil, err
}
inventory, err := mergeForwardingInventory(stored, runtime)
rules, err := manager.List()
if err != nil {
return 0, nil, err
}
items := make([]dto.ForwardRule, 0, len(rules))
keyword := strings.ToLower(strings.TrimSpace(request.Info))
filtered := inventory[:0]
for _, item := range inventory {
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
filtered = append(filtered, item)
for _, rule := range rules {
if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) {
continue
}
items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
}
inventory = filtered
total := len(inventory)
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
total := len(items)
if request.All {
start, end = 0, total
return int64(total), items, nil
}
if start > total {
return int64(total), make([]dto.ForwardRule, 0), nil
}
if end > total {
end = total
}
pageRules := inventory[start:end]
var items []dto.ForwardRule
if pageRules != nil {
items = make([]dto.ForwardRule, 0, len(pageRules))
}
for index, item := range pageRules {
items = append(items, dto.ForwardRule{
ID: item.ID,
Num: strconv.Itoa(start + index + 1),
Family: item.Rule.Family,
Protocol: item.Rule.Protocol,
Port: item.Rule.Port,
TargetIP: item.Rule.TargetIP,
TargetPort: item.Rule.TargetPort,
Interface: item.Rule.Interface,
IsDesired: item.IsDesired,
IsRuntime: item.IsRuntime,
SyncStatus: item.SyncStatus(),
})
}
return int64(total), items, nil
start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total)
end := min(start+max(request.PageSize, 1), total)
return int64(total), items[start:end], nil
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
@@ -198,7 +170,7 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
return s.operateRules(t.TaskCtx, request, t)
return s.operateRules(t.TaskCtx, request, t, false)
}, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
@@ -209,94 +181,142 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func (s *ForwardingService) Enable() error {
func (s *ForwardingService) Enable(ctx context.Context) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := s.clientFactory()
manager, err := s.clientFactory(ctx)
if err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.persistForwardingEnabled(); err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err)
return err
}
rules, err := s.rules.List(context.Background())
rules, err := manager.List()
if err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err)
return err
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if err := initializeForwarding(manager, families); err != nil {
return err
}
return persistForwardingRules(manager, rules)
}
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
}
var manager forwarding.Adapter
var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
var backup dto.FirewallSubsystemBackup
if request.BackupFile != "" {
var err error
manager, err = s.clientFactory()
backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding")
if err != nil {
recordForwardingSyncError(err)
return err
return dto.FilterChainOperationResponse{}, err
}
backend = manager.Name()
t.Logf("backend=%s", backend)
if err := s.persistForwardingEnabled(); err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err)
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
rules, err := s.rules.List(t.TaskCtx)
if err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err)
return err
}, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))}
for _, rule := range backup.Forwarding {
operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
}
return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error {
return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...)
})
}
func (s *ForwardingService) Restore(ctx context.Context) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
if err != nil || status != constant.StatusEnable {
return err
}
manager, err := s.clientFactory(ctx)
if err != nil {
return err
}
backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding")
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
before := len(backup.Forwarding)
backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 })
logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding))
}
missing := make(map[string]bool)
needsBind := false
for _, family := range families {
initialized, bound, err := manager.FamilyStatus(family)
if err != nil {
return err
}
missing[family] = !initialized
needsBind = needsBind || (initialized && !bound)
}
restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6)
for _, rule := range backup.Forwarding {
restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6
}
if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind {
return nil
}
current, err := manager.List()
if err != nil {
return err
}
seen := make(map[string]int, len(current))
for _, rule := range current {
seen[rule.Identity()]++
}
for _, family := range families {
if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 {
continue
}
if err := manager.OperateFamily(family, missing[family]); err != nil {
return err
}
}
for _, family := range families {
if !missing[family] {
continue
}
rules := make([]forwarding.Rule, 0)
for _, rule := range backup.Forwarding {
if rule.Family != family {
continue
}
if seen[rule.Identity()] > 0 {
seen[rule.Identity()]--
continue
}
rules = append(rules, rule)
}
if err := manager.CreateRules(ctx, rules); err != nil {
return err
}
}
return nil
}
func NewIForwardingService() IForwardingService {
return newForwardingService()
}
func loadForwardPolicy(command string) (string, error) {
func loadForwardPolicy(ctx context.Context, command string) (string, error) {
if !cmd.Which(command) {
command += "-nft"
if !cmd.Which(command) {
return "", nil
}
}
output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
if err != nil {
return "", err
}
@@ -311,256 +331,3 @@ func loadForwardPolicy(command string) (string, error) {
}
return "", errors.New("FORWARD default policy was not found")
}
func lastForwardingSyncError() string {
forwardingSyncStateMu.RLock()
defer forwardingSyncStateMu.RUnlock()
if forwardingLastSyncErr == nil {
return ""
}
return forwardingLastSyncErr.Error()
}
func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
byIdentity := make(map[string]int, len(stored)+len(runtime))
for _, record := range stored {
rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
TargetPort: record.TargetPort, Interface: record.Interface,
})
if err != nil {
return nil, fmt.Errorf("normalize desired forwarding rule: %w", err)
}
key := rule.Identity()
byIdentity[key] = len(items)
items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true})
}
for _, observed := range runtime {
rule, err := forwarding.NormalizeRule(observed)
if err != nil {
return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err)
}
key := rule.Identity()
if index, exists := byIdentity[key]; exists {
items[index].IsRuntime = true
continue
}
byIdentity[key] = len(items)
items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true})
}
return items, nil
}
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
values := []string{
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
}
for _, value := range values {
if strings.Contains(strings.ToLower(value), keyword) {
return true
}
}
return false
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task) (resultErr error) {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
type operationBatch struct {
operation forwarding.OperationType
rules []forwarding.Rule
}
groups := make([]operationBatch, 0)
for _, operation := range request.Rules {
kind := forwarding.OperationType(operation.Operation)
if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove {
return fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
}
if len(groups) == 0 || groups[len(groups)-1].operation != kind {
groups = append(groups, operationBatch{operation: kind})
}
for _, protocol := range strings.Split(operation.Protocol, "/") {
rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: operation.Family, Protocol: protocol, Port: operation.Port,
TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
})
if err != nil {
return err
}
groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
}
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
byIdentity := make(map[string]model.ForwardingRule, len(stored))
for index, rule := range forwardingRulesFromModels(stored) {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return err
}
byIdentity[normalized.Identity()] = stored[index]
}
succeeded, failed, skipped := 0, 0, 0
var nativeFailure error
defer func() {
recordForwardingSyncError(errors.Join(resultErr, nativeFailure))
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed}))
if skipped > 0 {
t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped)
}
}
}()
record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) {
label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
switch status {
case "skipped":
skipped++
if t != nil {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
}
case "failed":
failed++
if t != nil {
t.LogFailedWithErr(label, cause)
}
default:
succeeded++
if t != nil {
t.LogSuccess(label)
}
}
}
if len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd {
old := make(map[string]bool, len(groups[0].rules))
for _, rule := range groups[0].rules {
old[rule.Identity()] = true
}
unchanged := len(old) == len(groups[1].rules)
duplicate := false
for _, rule := range groups[1].rules {
key := rule.Identity()
unchanged = unchanged && old[key]
if _, exists := byIdentity[key]; exists && !old[key] {
duplicate = true
}
}
if unchanged || duplicate {
for _, group := range groups {
for _, rule := range group.rules {
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
}
}
return nil
}
for _, rule := range groups[1].rules {
if rule.Family != forwarding.FamilyIPv6 {
continue
}
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
if err != nil {
return fmt.Errorf("check IPv6 Router Advertisement: %w", err)
}
if len(interfaces) > 0 {
return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
}
break
}
}
var client forwarding.Adapter
var failures []error
for _, group := range groups {
byFamily := make(map[string][]forwarding.Rule, 2)
seen := make(map[string]bool, len(group.rules))
for _, rule := range group.rules {
key := rule.Identity()
_, exists := byIdentity[key]
if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
continue
}
seen[key] = true
byFamily[rule.Family] = append(byFamily[rule.Family], rule)
}
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
rules := byFamily[family]
if len(rules) == 0 {
continue
}
err := ctx.Err()
if err == nil && client == nil {
var enabled bool
enabled, err = s.forwardingEnabled()
if err == nil && !enabled {
err = fmt.Errorf("%w: forwarding is not initialized", filter.ErrProviderUnavailable)
}
if err == nil {
client, err = s.clientFactory()
}
}
if err == nil {
if group.operation == forwarding.OperationAdd {
err = client.CreateRules(ctx, rules)
} else {
err = client.DeleteRules(ctx, rules)
}
}
if err != nil {
nativeFailure = errors.Join(nativeFailure, err)
if !request.ForceDelete || !forwardingOperationsOnlyRemove(request.Rules) || ctx.Err() != nil {
failures = append(failures, err)
for _, rule := range rules {
record(group.operation, rule, "failed", err)
}
continue
}
if t != nil {
t.Logf("force delete database records: %v", err)
}
}
for start := 0; start < len(rules); start += 500 {
batch := rules[start:min(start+500, len(rules))]
records := make([]model.ForwardingRule, 0, len(batch))
ids := make([]uint, 0, len(batch))
for _, rule := range batch {
if group.operation == forwarding.OperationAdd {
records = append(records, model.ForwardingRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
} else if stored, exists := byIdentity[rule.Identity()]; exists {
ids = append(ids, stored.ID)
}
}
if group.operation == forwarding.OperationAdd {
err = s.rules.CreateBatch(context.WithoutCancel(ctx), records)
} else {
err = s.rules.DeleteBatch(context.WithoutCancel(ctx), ids)
}
if err != nil {
failures = append(failures, err)
}
for index, rule := range batch {
if err != nil {
record(group.operation, rule, "failed", err)
continue
}
if group.operation == forwarding.OperationAdd {
byIdentity[rule.Identity()] = records[index]
} else {
delete(byIdentity, rule.Identity())
}
record(group.operation, rule, "succeeded", nil)
}
}
}
if group.operation == forwarding.OperationRemove && len(failures) > 0 {
return errors.Join(failures...)
}
}
return errors.Join(failures...)
}
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive"
"github.com/moby/go-archive"
)
type ImageService struct{}
+437 -138
View File
@@ -33,11 +33,14 @@ import (
type MonitorService struct {
DiskIO chan ([]disk.IOCountersStat)
NetIO chan ([]net.IOCountersStat)
ctx context.Context
}
var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
monitorSettingMutex sync.Mutex
gpuMonitorMutex sync.Mutex
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
@@ -47,8 +50,13 @@ type IMonitorService interface {
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
LoadSetting() (*dto.MonitorSetting, error)
UpdateSetting(key, value string) error
CleanData() error
CleanData(monitorType string) error
LoadIOOptions() []string
LoadNetworkOptions() []string
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
@@ -60,6 +68,7 @@ func NewIMonitorService() IMonitorService {
return &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: context.Background(),
}
}
@@ -85,7 +94,7 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
base.TopCPUItems = processes
base.TopCPU = ""
}
if req.Param == "all" || req.Param == "mem" {
if req.Param == "all" || req.Param == "memory" {
var processes []dto.Process
_ = json.Unmarshal([]byte(base.TopMem), &processes)
base.TopMemItems = processes
@@ -96,7 +105,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "io" {
bases, err := monitorRepo.GetIO(repo.WithByName(req.IO), repo.WithByCreatedAt(req.StartTime, req.EndTime))
ioOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.IO) != 0 {
ioOpts = append(ioOpts, repo.WithByName(req.IO))
}
bases, err := monitorRepo.GetIO(ioOpts...)
if err != nil {
return nil, err
}
@@ -110,7 +123,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "network" {
bases, err := monitorRepo.GetNetwork(repo.WithByName(req.Network), repo.WithByCreatedAt(req.StartTime, req.EndTime))
netOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.Network) != 0 {
netOpts = append(netOpts, repo.WithByName(req.Network))
}
bases, err := monitorRepo.GetNetwork(netOpts...)
if err != nil {
return nil, err
}
@@ -126,117 +143,171 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
return data, nil
}
func (m *MonitorService) LoadIOOptions() []string {
optionSet := make(map[string]struct{})
if diskStat, err := disk.IOCounters(); err == nil {
for _, item := range diskStat {
optionSet[item.Name] = struct{}{}
}
}
// union with names recorded in the monitor db so removed devices stay selectable
if names, err := monitorRepo.GetIONames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadNetworkOptions() []string {
optionSet := make(map[string]struct{})
if netStat, err := net.IOCounters(true); err == nil {
for _, item := range netStat {
optionSet[item.Name] = struct{}{}
}
}
if names, err := monitorRepo.GetNetworkNames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
exist, client := accelerator.New()
if !exist {
return data
}
seen := make(map[string]bool)
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
}
data.Supported = true
devices, err := monitorRepo.GetGPUDevices()
if err != nil {
global.LOG.Warnf("Load accelerator history options failed: %v", err)
return data
}
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 {
hasGPUOrNPU = true
for _, device := range devices {
key := device.DeviceID
if key == "" {
key = "legacy:" + device.ProductName
}
}
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 seen[key] {
continue
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
optionType := "gpu"
if item.Kind == accelerator.KindXPU {
optionType = "xpu"
}
chartHide := dto.GPUChartHide{
ProductName: item.Label,
Type: optionType,
GPU: !item.Capabilities.Utilization,
Memory: !item.Capabilities.Memory,
Power: !item.Capabilities.Power,
PowerLimit: !item.Capabilities.PowerLimit,
Temperature: !item.Capabilities.Temperature,
Speed: !item.Capabilities.FanSpeed,
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
seen[key] = true
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)
}
return data
}
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
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 {
return data, err
}
for _, gpu := range gpuList {
data.Date = append(data.Date, gpu.CreatedAt)
data.GPUValue = append(data.GPUValue, gpu.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature)
data.PowerUsed = append(data.PowerUsed, gpu.PowerDraw)
data.PowerTotal = append(data.PowerTotal, gpu.MaxPowerLimit)
if gpu.MaxPowerLimit != 0 {
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100)
} else {
data.PowerPercent = append(data.PowerPercent, float64(0))
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
opts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByGPUDevice(req.DeviceID, req.ProductName, req.Legacy)}
data.SampleCount, err = monitorRepo.CountGPU(opts...)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
data.BucketSeconds = (seconds + 599) / 600
}
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)
}
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed)
if gpu.MemTotal != 0 {
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100)
} else {
data.MemoryPercent = append(data.MemoryPercent, float64(0))
} else {
for i, point := range points {
if i > 0 && points[i-1].IntervalSeconds > 0 {
interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
}
}
samples = append(samples, point)
}
var process []dto.GPUProcess
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil {
data.ProcessCount = append(data.ProcessCount, len(process))
data.GPUProcesses = append(data.GPUProcesses, process)
} else {
data.ProcessCount = append(data.ProcessCount, 0)
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{})
}
for _, point := range samples {
data.Date = append(data.Date, point.CreatedAt)
data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
data.JPEGUtil = append(data.JPEGUtil, point.JPEGUtil)
data.OFAUtil = append(data.OFAUtil, point.OFAUtil)
data.MediaUtil = append(data.MediaUtil, point.MediaUtil)
data.ComputeUtil = append(data.ComputeUtil, point.ComputeUtil)
data.CopyUtil = append(data.CopyUtil, point.CopyUtil)
data.HotspotTemperature = append(data.HotspotTemperature, point.HotspotTemperature)
data.FanRPM = append(data.FanRPM, point.FanRPM)
data.AICPUUtil = append(data.AICPUUtil, point.AICPUUtil)
data.CtrlCPUUtil = append(data.CtrlCPUUtil, point.CtrlCPUUtil)
data.DDRUsed = append(data.DDRUsed, point.DDRUsed)
data.DDRTotal = append(data.DDRTotal, point.DDRTotal)
data.HBMUsed = append(data.HBMUsed, point.HBMUsed)
data.HBMTotal = append(data.HBMTotal, point.HBMTotal)
data.DDRBandwidth = append(data.DDRBandwidth, point.DDRBandwidth)
data.HBMBandwidth = append(data.HBMBandwidth, point.HBMBandwidth)
data.MemoryBandwidth = append(data.MemoryBandwidth, point.MemoryBandwidth)
data.MediaFrequency = append(data.MediaFrequency, point.MediaFrequency)
data.HugepagesUsed = append(data.HugepagesUsed, point.HugepagesUsed)
data.HugepagesTotal = append(data.HugepagesTotal, point.HugepagesTotal)
data.GPUValue = append(data.GPUValue, point.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, point.Temperature)
data.MemoryTemperatureValue = append(data.MemoryTemperatureValue, point.MemoryTemperature)
data.PowerUsed = append(data.PowerUsed, point.PowerDraw)
data.PowerTotal = append(data.PowerTotal, point.MaxPowerLimit)
data.PowerPercent = append(data.PowerPercent, point.PowerPercent)
data.MemoryPercent = append(data.MemoryPercent, point.MemoryPercent)
data.MemoryTotal = append(data.MemoryTotal, point.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, point.MemUsed)
data.SpeedValue = append(data.SpeedValue, point.FanSpeed)
data.FrequencyValue = append(data.FrequencyValue, point.Frequency)
data.MemoryFrequencyValue = append(data.MemoryFrequencyValue, point.MemoryFrequency)
data.ProcessCount = append(data.ProcessCount, point.ProcessCount)
var processes []dto.GPUProcess
if data.BucketSeconds == 0 && point.ProcessCount != nil {
_ = json.Unmarshal([]byte(point.Processes), &processes)
}
data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed)
data.GPUProcesses = append(data.GPUProcesses, processes)
}
return data, nil
}
@@ -262,9 +333,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
}
func (m *MonitorService) UpdateSetting(key, value string) error {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
switch key {
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
if value != constant.StatusEnable && value != constant.StatusDisable {
return fmt.Errorf("invalid monitoring status")
}
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
interval, err := strconv.Atoi(value)
if err != nil || interval < 10 || interval > 43200 {
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
}
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
days, err := strconv.Atoi(value)
if err != nil || days < 1 {
return fmt.Errorf("monitoring retention must be a positive integer")
}
}
switch key {
case "MonitorStatus":
if value == constant.StatusEnable && global.MonitorCronID == 0 {
if value == constant.StatusEnable && monitorCancel == nil {
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
if err != nil {
return err
@@ -273,8 +362,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
return err
}
}
if value == constant.StatusDisable && global.MonitorCronID != 0 {
if value == constant.StatusDisable && monitorCancel != nil {
monitorCancel()
monitorCancel = nil
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
global.MonitorCronID = 0
}
@@ -283,31 +373,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
if err != nil {
return err
}
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
if status.Value == constant.StatusEnable && monitorCancel != nil {
if err := StartMonitor(true, value); err != nil {
return err
}
}
case "GPUMonitorStatus":
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
if err != nil {
return err
}
if err := StartGPUMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
global.GPUMonitorCronID = 0
}
case "GPUMonitorInterval":
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartGPUMonitor(value); err != nil {
return err
}
}
case "VLLMMonitorStatus":
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return err
}
if err := StartVLLMMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
global.VLLMMonitorCronID = 0
}
case "VLLMMonitorInterval":
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartVLLMMonitor(value); err != nil {
return err
}
}
}
return settingRepo.Update(key, value)
}
func (m *MonitorService) CleanData() error {
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
return err
func (m *MonitorService) CleanData(monitorType string) error {
switch monitorType {
case "host":
return monitorRepo.CleanHost()
case "gpu":
gpuMonitorMutex.Lock()
defer gpuMonitorMutex.Unlock()
return monitorRepo.CleanGPU()
default:
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
return err
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil {
return err
}
_ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error
return nil
}
func (m *MonitorService) Run() {
saveAcceleratorDataToDB()
if m.ctx.Err() != nil {
return
}
var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 {
@@ -353,30 +492,34 @@ func (m *MonitorService) Run() {
_ = monitorRepo.DelMonitorBase(timeForDelete)
_ = monitorRepo.DelMonitorIO(timeForDelete)
_ = monitorRepo.DelMonitorNet(timeForDelete)
_ = monitorRepo.DelMonitorGPU(timeForDelete)
}
func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters()
ioStat, _ := disk.IOCountersWithContext(m.ctx)
var diskIOList []disk.IOCountersStat
for _, io := range ioStat {
diskIOList = append(diskIOList, io)
}
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
m.DiskIO <- diskIOList
select {
case <-m.ctx.Done():
case m.DiskIO <- diskIOList:
}
}
func (m *MonitorService) loadNetIO() {
netStat, _ := net.IOCounters(true)
netStatAll, _ := net.IOCounters(false)
netStat, _ := net.IOCountersWithContext(m.ctx, true)
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
var netList []net.IOCountersStat
netList = append(netList, netStat...)
netList = append(netList, netStatAll...)
m.NetIO <- netList
select {
case <-m.ctx.Done():
case m.NetIO <- netList:
}
}
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
defer close(m.DiskIO)
for {
select {
case <-ctx.Done():
@@ -426,14 +569,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
}
}
_ = monitorRepo.BatchCreateMonitorIO(ioList)
m.DiskIO <- ioStat2
select {
case <-ctx.Done():
return
case m.DiskIO <- ioStat2:
}
}
}
}
}
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
defer close(m.NetIO)
for {
select {
case <-ctx.Done():
@@ -463,7 +609,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
}
_ = monitorRepo.BatchCreateMonitorNet(netList)
m.NetIO <- netStat2
select {
case <-ctx.Done():
return
case m.NetIO <- netStat2:
}
}
}
}
@@ -572,27 +722,38 @@ func loadTopMem() []dto.Process {
}
func StartMonitor(removeBefore bool, interval string) error {
intervalItem, err := strconv.Atoi(interval)
if err != nil || intervalItem < 10 || intervalItem > 43200 {
return fmt.Errorf("invalid host monitoring interval: %s", interval)
}
if removeBefore {
monitorCancel()
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
}
intervalItem, err := strconv.Atoi(interval)
if err != nil {
return err
}
service := NewIMonitorService()
ctx, cancel := context.WithCancel(context.Background())
monitorCancel = cancel
service := &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: ctx,
}
now := time.Now()
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
time.AfterFunc(time.Until(nextMinute), func() {
timer := time.AfterFunc(time.Until(nextMinute), func() {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
if ctx.Err() != nil {
return
}
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
if err != nil {
return
}
global.MonitorCronID = monitorID
})
monitorCancel = func() {
cancel()
timer.Stop()
}
service.Run()
@@ -602,7 +763,97 @@ func StartMonitor(removeBefore bool, interval string) error {
return nil
}
func saveAcceleratorDataToDB() {
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
hasGPUOrNPU := false
hasXPU := false
for _, item := range snapshot.Devices {
if item.Kind == accelerator.KindXPU {
hasXPU = true
} else {
hasGPUOrNPU = true
}
}
switch {
case hasGPUOrNPU && hasXPU:
data.GPUType = "mixed"
case hasXPU:
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
}
sort.Slice(snapshot.Devices, func(i, j int) bool {
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
chartHide := dto.GPUChartHide{
DeviceID: item.ID,
ProductName: item.Label,
Type: string(item.Kind),
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
}
return data
}
func StartGPUMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
}
global.GPUMonitorCronID = id
return nil
}
func (m *MonitorService) saveGPUData() {
if !gpuMonitorMutex.TryLock() {
return
}
defer gpuMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
return
}
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
}
exist, client := accelerator.New()
if !exist {
return
@@ -615,27 +866,64 @@ func saveAcceleratorDataToDB() {
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
}
intervalSeconds := 0
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
intervalSeconds, _ = strconv.Atoi(setting.Value)
}
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
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)
}
}
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
item := model.MonitorGPU{
ProductName: device.Label,
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
Temperature: device.Metrics.Temperature.ValueOrZero(),
PowerDraw: device.Metrics.Power.ValueOrZero(),
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
MemoryUtil: device.Metrics.MemoryUtil.Value,
MemoryActivity: device.Metrics.MemoryActivity.Value,
EncoderUtil: device.Metrics.EncoderUtil.Value,
DecoderUtil: device.Metrics.DecoderUtil.Value,
JPEGUtil: device.Metrics.JPEGUtil.Value,
OFAUtil: device.Metrics.OFAUtil.Value,
MediaUtil: device.Metrics.MediaUtil.Value,
ComputeUtil: device.Metrics.ComputeUtil.Value,
CopyUtil: device.Metrics.CopyUtil.Value,
HotspotTemperature: device.Metrics.HotspotTemperature.Value,
FanRPM: device.Metrics.FanRPM.Value,
AICPUUtil: device.Metrics.AICPUUtil.Value,
CtrlCPUUtil: device.Metrics.CtrlCPUUtil.Value,
DDRUsed: device.Metrics.DDRUsed.Value,
DDRTotal: device.Metrics.DDRTotal.Value,
HBMUsed: device.Metrics.HBMUsed.Value,
HBMTotal: device.Metrics.HBMTotal.Value,
DDRBandwidth: device.Metrics.DDRBandwidth.Value,
HBMBandwidth: device.Metrics.HBMBandwidth.Value,
MemoryBandwidth: device.Metrics.MemoryBandwidth.Value,
MediaFrequency: device.Metrics.MediaFrequency.Value,
HugepagesUsed: device.Metrics.HugepagesUsed.Value,
HugepagesTotal: device.Metrics.HugepagesTotal.Value,
ProductName: device.Label,
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
}
processes := make([]dto.GPUProcess, 0, len(device.Processes))
@@ -695,3 +983,14 @@ func loadHostSysPath() string {
}
return hostSys
}
func sortedMonitorOptions(optionSet map[string]struct{}) []string {
options := make([]string, 0, len(optionSet))
for name := range optionSet {
if len(name) != 0 && name != "all" {
options = append(options, name)
}
}
sort.Strings(options)
return append([]string{"all"}, options...)
}
+287
View File
@@ -0,0 +1,287 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/robfig/cron/v3"
"golang.org/x/sync/errgroup"
)
var vllmMonitorMutex sync.Mutex
var vllmMetricsClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request")
}
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return data, err
}
if install.App.Key != "vllm" {
return data, fmt.Errorf("not a vLLM installation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
}
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
if err != nil {
return data, err
}
if data.BucketSeconds > 0 {
if req.Aggregation != "max" {
for i := range points {
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
return data, err
}
}
}
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
var point model.MonitorVLLM
if next < len(points) && points[next].Bucket == bucket {
point = points[next].MonitorVLLM
next++
}
point.AppInstallID = req.AppInstallID
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
data.Points = append(data.Points, point.MonitorVLLM)
}
}
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
}
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return previous, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return model.MonitorVLLM{}, ctx.Err()
case <-timer.C:
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
}
return point, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "vllm" {
return fmt.Errorf("not a vLLM installation")
}
vllmMonitorMutex.Lock()
defer vllmMonitorMutex.Unlock()
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
}
func StartVLLMMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
}
global.VLLMMonitorCronID = id
return nil
}
func (m *MonitorService) saveVLLMData() {
if !vllmMonitorMutex.TryLock() {
return
}
defer vllmMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
return
}
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
}
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
if err != nil {
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
return
}
if len(apps) == 0 {
return
}
ids := make([]uint, 0, len(apps))
for _, app := range apps {
ids = append(ids, app.ID)
}
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
if err != nil {
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
return
}
var group errgroup.Group
group.SetLimit(4)
for _, install := range installs {
group.Go(func() error {
previous, err := vllmMonitorRepo.Latest(install.ID)
if err != nil {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
return vllmMonitorRepo.Create(&point)
})
}
if err := group.Wait(); err != nil {
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
}
}
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
if install.HttpPort <= 0 || install.HttpPort > 65535 {
return point, fmt.Errorf("invalid vLLM port")
}
var env map[string]interface{}
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
return point, err
}
host, _ := env[constant.HostIP].(string)
host = strings.Trim(host, "[]")
switch host {
case "", "0.0.0.0":
host = "127.0.0.1"
case "::":
host = "::1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
if err != nil {
return point, err
}
response, err := vllmMetricsClient.Do(req)
if err != nil {
return point, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
if err != nil {
return point, err
}
if len(body) > 8*1024*1024 {
return point, fmt.Errorf("vLLM metrics response too large")
}
metrics, err := vllm.Parse(bytes.NewReader(body))
if err != nil {
return point, err
}
point.CreatedAt = time.Now()
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
}
var before vllm.Metrics
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
if previous.Status == "ok" && elapsed > 0 {
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
return point, err
}
}
calculated, err := vllm.Calculate(metrics, before, elapsed)
if err != nil {
return point, err
}
calculated.AppInstallID = install.ID
calculated.CreatedAt = point.CreatedAt
calculated.RawMetrics = string(raw)
calculated.Status = "ok"
return calculated, nil
}
+2
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
@@ -44,6 +45,7 @@ type ISettingService interface {
GetLocalConnForSSH() (dto.SSHConnData, error)
SaveDescription(req dto.CommonDescription) error
CleanupDescriptions(context.Context) (int64, error)
}
func NewISettingService() ISettingService {
+114
View File
@@ -0,0 +1,114 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"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/lifecycle"
"github.com/docker/docker/api/types/container"
)
func (u *SettingService) CleanupDescriptions(ctx context.Context) (int64, error) {
var deleted int64
var failures []error
for _, kind := range []string{"container", "firewall", "firewall-docker"} {
count, err := func() (int64, error) {
switch kind {
case "container":
return cleanupUnusedDescriptions(ctx, kind, loadContainerDescriptionIDs)
case "firewall":
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return cleanupUnusedDescriptions(ctx, kind, loadHostFirewallDescriptionIDs)
default:
return cleanupUnusedDescriptions(ctx, kind, nil)
}
}()
deleted += count
if err != nil {
failures = append(failures, fmt.Errorf("%s: %w", kind, err))
}
}
return deleted, errors.Join(failures...)
}
func cleanupUnusedDescriptions(ctx context.Context, kind string, loadIDs func(context.Context) (map[string]bool, error)) (int64, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
descriptions, err := settingRepo.GetDescriptionList(repo.WithByType(kind))
if err != nil || len(descriptions) == 0 {
return 0, err
}
var active map[string]bool
if loadIDs != nil {
active, err = loadIDs(ctx)
if err != nil {
return 0, err
}
}
ids, empty := make([]string, 0), make([]string, 0)
for _, description := range descriptions {
if !active[description.ID] {
ids = append(ids, description.ID)
} else if description.Description == "" && !description.IsPinned {
empty = append(empty, description.ID)
}
}
deleted, err := settingRepo.DeleteDescriptions(ctx, kind, ids, false)
if err != nil {
return deleted, err
}
count, err := settingRepo.DeleteDescriptions(ctx, kind, empty, true)
return deleted + count, err
}
func loadContainerDescriptionIDs(ctx context.Context) (map[string]bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, err
}
ids := make(map[string]bool, len(containers))
for _, item := range containers {
ids[item.ID] = true
}
return ids, nil
}
func loadHostFirewallDescriptionIDs(ctx context.Context) (map[string]bool, error) {
providers := lifecycle.InstalledProviders()
if len(providers) == 0 {
return nil, filter.ErrProviderUnavailable
}
service := newFirewallService()
ids := make(map[string]bool)
for _, name := range providers {
provider := filter.Provider(name)
inventory, err := service.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider))
if err != nil {
return nil, err
}
for _, notice := range inventory.Notices {
if notice.Code == filter.ScopeNoticeFamilyUnavailable || notice.Code == filter.ScopeNoticeManagedScopeInactive {
return nil, fmt.Errorf("%w: %s %s", filter.ErrInventoryUnavailable, name, notice.Code)
}
}
for _, item := range inventory.Items {
id, err := filter.DescriptionID(*item.Observed)
if err != nil {
return nil, err
}
ids[id] = true
}
}
return ids, nil
}
+72 -11
View File
@@ -7,6 +7,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
@@ -37,6 +38,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
const sshPath = "/etc/ssh/sshd_config"
@@ -438,15 +440,33 @@ func (u *SSHService) SyncRootCert() error {
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
continue
}
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
continue
}
cert.EncryptionMode = loadSSHKeyType(publicKey)
if cert.EncryptionMode == "" {
continue
}
rootCerts = append(rootCerts, cert)
}
return hostRepo.SyncCert(rootCerts)
}
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
return buserr.New("ErrCmdIllegal")
switch req.Mode {
case "generate":
switch req.EncryptionMode {
case "rsa", "ed25519", "ecdsa", "dsa":
default:
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
}
case "input", "import":
if err := validateSSHKeyPair(&req); err != nil {
return err
}
default:
return buserr.WithName("ErrNotSupportType", req.Mode)
}
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
if certItem.ID != 0 {
@@ -543,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
}
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
if err := validateSSHKeyPair(&req); err != nil {
return err
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("load current user failed, err: %v", err)
@@ -1681,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
return itemDate
}
func loadEncryptioMode(content string) string {
if strings.HasPrefix(content, "ssh-rsa") {
func loadSSHKeyType(publicKey ssh.PublicKey) string {
switch publicKey.Type() {
case ssh.KeyAlgoRSA:
return "rsa"
}
if strings.HasPrefix(content, "ssh-ed25519") {
case ssh.KeyAlgoED25519:
return "ed25519"
}
if strings.HasPrefix(content, "ssh-ecdsa") {
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
return "ecdsa"
}
if strings.HasPrefix(content, "ssh-dsa") {
case ssh.KeyAlgoDSA:
return "dsa"
}
return ""
}
func validateSSHKeyPair(req *dto.RootCertOperate) error {
req.PublicKey = strings.TrimSpace(req.PublicKey)
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
return buserr.New("ErrSSHPublicKey")
}
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPublicKey")
}
req.EncryptionMode = loadSSHKeyType(publicKey)
if req.EncryptionMode == "" {
return buserr.WithName("ErrNotSupportType", publicKey.Type())
}
block, rest := pem.Decode([]byte(req.PrivateKey))
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPrivateKey")
}
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
var missingPassphrase *ssh.PassphraseMissingError
if errors.As(err, &missingPassphrase) {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
} else if err == nil {
req.PassPhrase = ""
}
if err != nil {
return buserr.New("ErrSSHPrivateKey")
}
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
return buserr.New("ErrSSHKeyMismatch")
}
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
if comment != "" {
req.PublicKey += " " + comment
}
req.PublicKey += "\n"
req.PrivateKey += "\n"
return nil
}
func updateLocalConn(newPort uint) error {
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
if len(conn) == 0 {
+2 -1
View File
@@ -99,6 +99,7 @@ const (
TaskScopeTamper = "Tamper"
TaskScopeFileConvert = "Convert"
TaskScopeTask = "Task"
TaskScopeVm = "VirtualMachine"
)
func GetTaskName(resourceName, operate, scope string) string {
@@ -154,7 +155,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
logger := logrus.New()
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 {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
+122 -107
View File
@@ -265,8 +265,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
"formatEN": "Application port update [key]-[name] =\u003e [port]"
"formatZH": "应用端口修改 [key]-[name] => [port]",
"formatEN": "Application port update [key]-[name] => [port]"
},
"/apps/installed/sort/update": {
"bodyKeys": [],
@@ -679,8 +679,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "容器重命名 [name] =\u003e [newName]",
"formatEN": "rename container [name] =\u003e [newName]"
"formatZH": "容器重命名 [name] => [newName]",
"formatEN": "rename container [name] => [newName]"
},
"/containers/repo": {
"bodyKeys": [
@@ -831,8 +831,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
},
"/core/auth/expired/reset": {
"bodyKeys": [],
@@ -1716,8 +1716,8 @@
"output_value": "name"
}
],
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
},
"/core/enterprise/users/del": {
"bodyKeys": [
@@ -1918,8 +1918,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
},
"/core/settings/memo": {
"bodyKeys": [],
@@ -1948,8 +1948,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改系统端口 =\u003e [serverPort]",
"formatEN": "update system port =\u003e [serverPort]"
"formatZH": "修改系统端口 => [serverPort]",
"formatEN": "update system port => [serverPort]"
},
"/core/settings/proxy/update": {
"bodyKeys": [
@@ -1974,8 +1974,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改系统 ssl =\u003e [ssl]",
"formatEN": "update system ssl =\u003e [ssl]"
"formatZH": "修改系统 ssl => [ssl]",
"formatEN": "update system ssl => [ssl]"
},
"/core/settings/terminal/update": {
"bodyKeys": [],
@@ -1991,8 +1991,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] =\u003e [value]"
"formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] => [value]"
},
"/core/settings/upgrade": {
"bodyKeys": [
@@ -2000,8 +2000,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新系统 =\u003e [version]",
"formatEN": "upgrade system =\u003e [version]"
"formatZH": "更新系统 => [version]",
"formatEN": "upgrade system => [version]"
},
"/core/xapp/appQRCode": {
"bodyKeys": [
@@ -2459,7 +2459,7 @@
"formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]"
},
"/core/xpack/vms": {
"/xpack/vms": {
"bodyKeys": [
"name"
],
@@ -2468,7 +2468,7 @@
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/xpack/vms/del": {
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
@@ -2486,14 +2486,25 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/environment/enable": {
"/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/xpack/vms/iso": {
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
@@ -2503,7 +2514,7 @@
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/xpack/vms/iso/del": {
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
@@ -2521,7 +2532,7 @@
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/xpack/vms/iso/update": {
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
@@ -2531,7 +2542,7 @@
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/xpack/vms/networks": {
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
@@ -2541,7 +2552,7 @@
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/xpack/vms/networks/del": {
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
@@ -2559,7 +2570,7 @@
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/xpack/vms/networks/update": {
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
@@ -2577,7 +2588,7 @@
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/xpack/vms/operate": {
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
@@ -2587,14 +2598,14 @@
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/xpack/vms/orphans/clean": {
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/xpack/vms/rename": {
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
@@ -2604,7 +2615,7 @@
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/xpack/vms/snapshots": {
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
@@ -2614,7 +2625,7 @@
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/del": {
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
@@ -2624,7 +2635,7 @@
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/recover": {
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
@@ -2634,7 +2645,7 @@
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/storages": {
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
@@ -2644,7 +2655,7 @@
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/xpack/vms/storages/del": {
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
@@ -2662,7 +2673,7 @@
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/xpack/vms/storages/update": {
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
@@ -2680,14 +2691,14 @@
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/xpack/vms/sync": {
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/xpack/vms/templates": {
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
@@ -2697,7 +2708,7 @@
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/xpack/vms/templates/del": {
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
@@ -2715,7 +2726,7 @@
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/xpack/vms/update": {
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
@@ -2877,8 +2888,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
"formatEN": "app launcher [key] =\u003e show: [value]"
"formatZH": "首页应用 [key] => 显示:[value]",
"formatEN": "app launcher [key] => show: [value]"
},
"/dashboard/quick/change": {
"bodyKeys": [],
@@ -3006,7 +3017,7 @@
}
],
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
"formatEN": "The description of the mysql database [name] is modified => [description]"
},
"/databases/grants": {
"bodyKeys": [
@@ -3087,7 +3098,7 @@
}
],
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
"formatEN": "The description of the mongodb database [name] is modified => [description]"
},
"/databases/mongodb/password": {
"bodyKeys": [
@@ -3172,7 +3183,7 @@
}
],
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
"formatEN": "The description of the postgresql database [name] is modified => [description]"
},
"/databases/pg/password": {
"bodyKeys": [
@@ -3277,8 +3288,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
},
"/databases/variables/update": {
"bodyKeys": [],
@@ -3314,8 +3325,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
},
"/files/chunkdownload": {
"bodyKeys": [
@@ -3405,8 +3416,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改权限 [path] =\u003e [mode]",
"formatEN": "Change mode [path] =\u003e [mode]"
"formatZH": "修改权限 [path] => [mode]",
"formatEN": "Change mode [path] => [mode]"
},
"/files/move": {
"bodyKeys": [
@@ -3415,8 +3426,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
"formatEN": "Move [oldPaths] =\u003e [newPath]"
"formatZH": "移动文件 [oldPaths] => [newPath]",
"formatEN": "Move [oldPaths] => [newPath]"
},
"/files/owner": {
"bodyKeys": [
@@ -3426,8 +3437,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
"formatEN": "Change owner [path] =\u003e [user]/[group]"
"formatZH": "修改用户/组 [path] => [user]/[group]",
"formatEN": "Change owner [path] => [user]/[group]"
},
"/files/preview": {
"bodyKeys": [
@@ -3461,8 +3472,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名 [oldName] =\u003e [newName]",
"formatEN": "Rename [oldName] =\u003e [newName]"
"formatZH": "重命名 [oldName] => [newName]",
"formatEN": "Rename [oldName] => [newName]"
},
"/files/save": {
"bodyKeys": [
@@ -3519,8 +3530,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载 url =\u003e [path]/[name]",
"formatEN": "Download url =\u003e [path]/[name]"
"formatZH": "下载 url => [path]/[name]",
"formatEN": "Download url => [path]/[name]"
},
"/files/wget/process/remove": {
"bodyKeys": [
@@ -3670,12 +3681,16 @@
"formatZH": "删除 Docker 端口防护策略 [uuids]",
"formatEN": "delete Docker port guard policies [uuids]"
},
"/hosts/firewall/docker/sync": {
"bodyKeys": [],
"/hosts/firewall/family/operate": {
"bodyKeys": [
"subsystem",
"family",
"operation"
],
"formatEN": "[operation] [subsystem] [family] firewall chains",
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步 Docker 端口防护规则",
"formatEN": "sync Docker port guard rules"
"beforeFunctions": []
},
"/hosts/firewall/filter/operate": {
"bodyKeys": [
@@ -3716,13 +3731,6 @@
"formatZH": "添加防火墙规则",
"formatEN": "create firewall rules"
},
"/hosts/firewall/rules/adopt": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
},
"/hosts/firewall/rules/delete": {
"bodyKeys": [],
"paramKeys": [],
@@ -3732,12 +3740,12 @@
},
"/hosts/firewall/rules/reorder": {
"bodyKeys": [
"uuid"
"instanceKey"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [uuid]",
"formatEN": "reorder firewall rule [uuid]"
"formatZH": "调整防火墙规则顺序 [instanceKey]",
"formatEN": "reorder firewall rule [instanceKey]"
},
"/hosts/firewall/rules/reset": {
"bodyKeys": [],
@@ -3746,25 +3754,23 @@
"formatZH": "重置防火墙规则",
"formatEN": "reset firewall rules"
},
"/hosts/firewall/rules/sync": {
"bodyKeys": [
"subsystem",
"sourceProvider",
"targetProvider"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]",
"formatEN": "sync [subsystem] firewall rules to [targetProvider]"
},
"/hosts/firewall/rules/update": {
"bodyKeys": [
"uuid"
"instanceKey"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防火墙规则 [uuid]",
"formatEN": "update firewall rule [uuid]"
"formatZH": "更新防火墙规则 [instanceKey]",
"formatEN": "update firewall rule [instanceKey]"
},
"/hosts/firewall/settings/ipv6": {
"bodyKeys": [
"status"
],
"formatEN": "Set firewall IPv6 support to [status]",
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"paramKeys": [],
"beforeFunctions": []
},
"/hosts/firewall/settings/operate": {
"bodyKeys": [
@@ -3882,8 +3888,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
"formatEN": "update SSH setting [key] =\u003e [newValue]"
"formatZH": "修改 SSH 配置 [key] => [newValue]",
"formatEN": "update SSH setting [key] => [newValue]"
},
"/hosts/tool/config/set": {
"bodyKeys": [
@@ -4028,12 +4034,21 @@
},
"/runtimes/operate": {
"bodyKeys": [
"id"
"ID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作运行环境 [id]",
"formatEN": "Operate runtime [id]"
"beforeFunctions": [
{
"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": {
"bodyKeys": [
@@ -4135,7 +4150,7 @@
}
],
"formatZH": "快照 [name] 描述信息修改 [description]",
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
"formatEN": "The description of the snapshot [name] is modified => [description]"
},
"/settings/snapshot/import": {
"bodyKeys": [
@@ -4227,8 +4242,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] =\u003e [value]"
"formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] => [value]"
},
"/toolbox/clam": {
"bodyKeys": [
@@ -4346,8 +4361,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改主机参数 [key] =\u003e [value]",
"formatEN": "update device conf [key] =\u003e [value]"
"formatZH": "修改主机参数 [key] => [value]",
"formatEN": "update device conf [key] => [value]"
},
"/toolbox/device/update/host": {
"bodyKeys": [
@@ -4356,8 +4371,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改主机 Host [key] =\u003e [value]",
"formatEN": "update device host [key] =\u003e [value]"
"formatZH": "修改主机 Host [key] => [value]",
"formatEN": "update device host [key] => [value]"
},
"/toolbox/device/update/swap": {
"bodyKeys": [
@@ -4385,8 +4400,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
"formatEN": "update fail2ban conf [key] =\u003e [value]"
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
"formatEN": "update fail2ban conf [key] => [value]"
},
"/toolbox/ftp": {
"bodyKeys": [
@@ -4645,8 +4660,8 @@
"output_value": "domain"
}
],
"formatZH": "修改默认 server =\u003e [domain]",
"formatEN": "Change default server =\u003e [domain]"
"formatZH": "修改默认 server => [domain]",
"formatEN": "Change default server => [domain]"
},
"/websites/del": {
"bodyKeys": [
@@ -5409,8 +5424,8 @@
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
"formatEN": "update tamper info [website][path] =\u003e [status]"
"formatZH": "更新防篡改信息 [website][path] => [status]",
"formatEN": "update tamper info [website][path] => [status]"
},
"/xpack/vllm/command-template/create": {
"bodyKeys": [
+1 -5
View File
@@ -17,6 +17,7 @@ const (
)
const (
FirewallIPv6SupportKey = "FirewallIPv6Support"
FirewallSystemBackendKey = "FirewallProvider"
FirewallForwardingBackendKey = "ForwardingBackend"
FirewallDockerBackendKey = "DockerFirewallBackend"
@@ -33,12 +34,7 @@ const (
const (
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
FirewallRuleOriginCreated = "created"
FirewallRuleOriginAdopted = "adopted"
FirewallRuleSourceUser = "user"
FirewallRuleSourceImported = "imported"
FirewallRuleSourcePanel = "panel"
FirewallRuleSourceSecurity = "security"
FirewallRuleSourceApp = "application"
)
+4
View File
@@ -1,6 +1,10 @@
package constant
const (
StatusInactive = "Inactive"
StatusUnknown = "Unknown"
StatusDegraded = "Degraded"
StatusInaccessible = "Inaccessible"
StatusRunning = "Running"
StatusCanceled = "Canceled"
StatusDone = "Done"
+34
View File
@@ -1,6 +1,7 @@
package cron
import (
"context"
"crypto/rand"
"fmt"
"math/big"
@@ -38,6 +39,28 @@ func Run() {
}
}
var gpuStatus, gpuInterval model.Setting
if err := global.DB.Where("key = ?", "GPUMonitorStatus").First(&gpuStatus).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
} else if gpuStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "GPUMonitorInterval").First(&gpuInterval).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring interval failed: %v", err)
} else if err := service.StartGPUMonitor(gpuInterval.Value); err != nil {
global.LOG.Errorf("Start GPU monitoring failed: %v", err)
}
}
var vllmStatus, vllmInterval model.Setting
if err := global.DB.Where("key = ?", "VLLMMonitorStatus").First(&vllmStatus).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
} else if vllmStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "VLLMMonitorInterval").First(&vllmInterval).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring interval failed: %v", err)
} else if err := service.StartVLLMMonitor(vllmInterval.Value); err != nil {
global.LOG.Errorf("Start vLLM monitoring failed: %v", err)
}
}
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
global.LOG.Errorf("can not add website corn job: %s", err.Error())
}
@@ -60,6 +83,17 @@ func Run() {
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
global.LOG.Errorf("can not add backup token refresh corn job: %s", err.Error())
}
if _, err := global.Cron.AddFunc("@every 240h", func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
deleted, err := service.NewISettingService().CleanupDescriptions(ctx)
if err != nil {
global.LOG.Errorf("clean unused common descriptions: %v", err)
}
global.LOG.Infof("common description cleanup completed, deleted %d records", deleted)
}); err != nil {
global.LOG.Errorf("add common description cleanup job: %v", err)
}
var cronJobs []model.Cronjob
if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil {
+11 -8
View File
@@ -14,12 +14,13 @@ import (
)
var (
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
VLLMMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
LOG *logrus.Logger
CONF ServerConfig
@@ -29,8 +30,10 @@ var (
Dir SystemDir
Cron *cron.Cron
MonitorCronID cron.EntryID
Cron *cron.Cron
MonitorCronID cron.EntryID
VLLMMonitorCronID cron.EntryID
GPUMonitorCronID cron.EntryID
IsMaster bool
+17 -20
View File
@@ -1,14 +1,12 @@
module github.com/1Panel-dev/1Panel/agent
go 1.26.1
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
go 1.26.6
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.14.0
github.com/compose-spec/compose-go/v2 v2.15.0
github.com/creack/pty v1.1.24
github.com/docker/cli v29.7.2+incompatible
github.com/docker/docker v28.5.2+incompatible
@@ -33,6 +31,7 @@ require (
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.73
github.com/minio/minio-go/v7 v7.3.0
github.com/moby/go-archive v0.3.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1
@@ -52,13 +51,13 @@ require (
github.com/tklauser/go-sysconf v0.4.0
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.8.2
golang.org/x/crypto v0.55.0
go.mongodb.org/mongo-driver/v2 v2.9.0
golang.org/x/crypto v0.56.0
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
golang.org/x/time v0.15.0
golang.org/x/time v0.16.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3
gopkg.in/yaml.v3 v3.0.1
@@ -121,7 +120,7 @@ require (
github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -132,7 +131,6 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -155,11 +153,10 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -209,18 +206,18 @@ require (
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/grpc v1.83.1 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
+41 -40
View File
@@ -205,8 +205,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00=
github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -307,8 +307,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -438,6 +438,7 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
@@ -597,16 +598,16 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -877,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0=
go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -889,23 +890,23 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -951,8 +952,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1076,8 +1077,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1193,8 +1194,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1315,10 +1316,10 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1336,8 +1337,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+51 -17
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."
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."
CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details."
# disk
DeviceNotFound: "Device {{ .name }} not found"
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD defaults to DROP. Adjust the policy and retry'
ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again'
ErrDockerForwardPolicyDrop: 'This server restricts network forwarding. Container port protection cannot be enabled at this time.'
Firewall: 'Firewall'
FirewallTaskHost: 'Host firewall'
FirewallTaskForwarding: 'Port forwarding'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Create firewall rules'
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
FirewallCreateRuleSkipped: 'Not executed'
FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules'
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
FirewallCreateRuleExecutionFailed: 'Rule creation failed; executed commands were not rolled back'
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}'
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
FirewallEnableForwardingStep: 'Enable and bind port forwarding chains'
FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
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."
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"
FileTaskCopy: 'Copy files to {{ .dst }}'
FileTaskMove: 'Move files to {{ .dst }}'
FileTaskCompress: 'Compress files to {{ .dst }}'
@@ -726,7 +713,54 @@ 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 }}"
ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead."
ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry."
ErrSSHPublicKey: "Enter a valid SSH public key"
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
# virtual machine
ErrVMNotFound: "Virtual machine does not exist"
ErrVMAlreadyExists: "Virtual machine already exists"
ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more."
ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm not found"
ErrVMBridgeManaged: "Bridge name is already managed by libvirt or Docker"
ErrVMNetworkInUse: "Network is in use and cannot be deleted"
ErrVMStorageInUse: "Storage pool is in use and cannot be deleted"
ErrVMStorageNameCannotChange: "Storage pool name cannot be changed"
ErrVMStoragePathCannotChange: "Storage pool path cannot be changed"
ErrVMRunning: "Virtual machine is running"
ErrVMLibvirtConnect: "Failed to connect to libvirt: {{ .detail }}"
ErrVMPermission: "No permission to operate virtual machine: {{ .detail }}"
ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
VMCreateTask: "Create virtual machine [{{ .name }}]"
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
VMUpdateTask: "Update virtual machine [{{ .name }}]"
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
VMTemplateDeleteTask: "Delete virtual machine template [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm not found, force deleting virtual machine [{{ .name }}] metadata only"
VMForceDeleteMetadataAfterLibvirtFailed: "Failed to delete virtual machine [{{ .name }}] from libvirt, force deleting metadata: {{ .err }}"
VMForceDeleteDiskFailed: "Force delete virtual machine disk file [{{ .path }}] failed: {{ .err }}"
VMForceDeleteDiskSuccess: "Force deleted virtual machine disk file [{{ .path }}]"
VMSnapshotCreateTask: "Create snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotCreateStep: "Create virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
+51 -17
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.'
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.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} no encontrado'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo'
ErrDockerForwardPolicyDrop: 'La política predeterminada de FORWARD para {{ .family }} es DROP. Ajústela y vuelva a intentarlo'
ErrUFWRuleAdopt: 'No se pudo administrar la regla UFW: {{ .detail }}. Si la versión actual de UFW es anterior a 0.35, actualice UFW y vuelva a intentarlo'
ErrDockerForwardPolicyDrop: 'Este servidor restringe el reenvío de red. Por ahora no se puede habilitar la protección de puertos de contenedores.'
Firewall: 'Firewall'
FirewallTaskHost: 'Cortafuegos del host'
FirewallTaskForwarding: 'Reenvío de puertos'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
FirewallCreateRuleSkipped: 'Sin ejecutar'
FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas'
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. Los comandos ejecutados no se revirtieron'
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar las reglas de la base de datos en {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos'
FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos de la base de datos'
FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas'
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
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"
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 }}'
@@ -726,7 +713,54 @@ 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 }}"
ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente."
ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo."
ErrSSHPublicKey: "Introduce una clave pública SSH válida"
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
# máquina virtual
ErrVMNotFound: "La máquina virtual no existe"
ErrVMAlreadyExists: "La máquina virtual ya existe"
ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo."
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
ErrVMHelperNotFound: "No se encontró 1panel-kvm"
ErrVMBridgeManaged: "El nombre del bridge ya está administrado por libvirt o Docker"
ErrVMNetworkInUse: "La red está en uso y no se puede eliminar"
ErrVMStorageInUse: "El pool de almacenamiento está en uso y no se puede eliminar"
ErrVMStorageNameCannotChange: "El nombre del pool de almacenamiento no se puede cambiar"
ErrVMStoragePathCannotChange: "La ruta del pool de almacenamiento no se puede cambiar"
ErrVMRunning: "La máquina virtual está en ejecución"
ErrVMLibvirtConnect: "Error al conectar con libvirt: {{ .detail }}"
ErrVMPermission: "Sin permiso para operar la máquina virtual: {{ .detail }}"
ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
VMTemplateDeleteTask: "Eliminar plantilla de máquina virtual [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm no encontrado, eliminando solo los metadatos de la máquina virtual [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Error al eliminar la máquina virtual [{{ .name }}] de libvirt, eliminando metadatos: {{ .err }}"
VMForceDeleteDiskFailed: "Error al forzar la eliminación del archivo de disco de la máquina virtual [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Archivo de disco de la máquina virtual [{{ .path }}] eliminado forzosamente"
VMSnapshotCreateTask: "Crear instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotCreateStep: "Crear instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
+52 -17
View File
@@ -649,6 +649,8 @@ CommonAlert: "پنل {{ .node }}{{ .ip }}: {{ .msg }}. برای مشاهده ج
NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید."
LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید."
SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید."
CronJobSuccessAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی‌شده {{ .name }} با موفقیت تکمیل شد. برای مشاهده جزئیات وارد شوید."
# دیسک
DeviceNotFound: "دستگاه {{ .name }} یافت نشد"
@@ -682,8 +684,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید'
ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید'
ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید'
ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید'
ErrDockerForwardPolicyDrop: 'این سرور هدایت ترافیک شبکه را محدود کرده است. در حال حاضر امکان فعال‌سازی محافظت از پورت کانتینرها وجود ندارد.'
Firewall: 'فایروال'
FirewallTaskHost: 'دیواره آتش میزبان'
FirewallTaskForwarding: 'هدایت پورت'
@@ -697,28 +698,15 @@ FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش'
FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده'
FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق'
FirewallCreateRuleSkipped: 'اجرا نشده'
FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دسته‌ای به {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد'
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. دستورات اجراشده بازگردانی نشدند'
FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}'
FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}'
FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال'
FirewallEnableForwardingStep: 'فعال‌سازی و اتصال زنجیره‌های هدایت پورت'
FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده'
FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها'
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}'
FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}'
FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}'
@@ -726,7 +714,54 @@ FileTaskDecompress: 'استخراج فایل‌ها در {{ .dst }}'
FileTaskSource: 'مسیر مبدأ: {{ .path }}'
FileTaskFormat: 'قالب بایگانی: {{ .format }}'
FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند"
ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند"
# ماشین مجازی
ErrVMNotFound: "ماشین مجازی وجود ندارد"
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است"
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
ErrVMBridgeManaged: "نام bridge از قبل توسط libvirt یا Docker مدیریت می‌شود"
ErrVMNetworkInUse: "شبکه در حال استفاده است و قابل حذف نیست"
ErrVMStorageInUse: "مخزن ذخیره‌سازی در حال استفاده است و قابل حذف نیست"
ErrVMStorageNameCannotChange: "نام مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMStoragePathCannotChange: "مسیر مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMRunning: "ماشین مجازی در حال اجرا است"
ErrVMLibvirtConnect: "اتصال به libvirt ناموفق بود: {{ .detail }}"
ErrVMPermission: "مجوز عملیات ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMNetworkNotFound: "شبکه ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMInvalidPath: "مسیر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMInvalidInput: "پارامتر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMCommandFailed: "اجرای دستور ماشین مجازی ناموفق بود: {{ .err }}"
ErrVMDiskResizeBlocked: "دیسک شرایط افزایش ظرفیت را ندارد یا دستگاه دیگری به آن ارجاع می‌دهد. وضعیت دیسک را بررسی کنید."
ErrVMDiskReferenced: "این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود."
ErrVMDiskReferenceCheck: "بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید."
ErrVMSnapshotMetadataSync: "اسنپ‌شات بازیابی شد، اما همگام‌سازی اطلاعات دیسک ناموفق بود. تازه‌سازی و بررسی کنید؛ نیازی به بازیابی دوباره اسنپ‌شات نیست."
VMCreateTask: "ایجاد ماشین مجازی [{{ .name }}]"
VMCreateFromTemplateTask: "ایجاد ماشین مجازی [{{ .name }}] از قالب"
VMUpdateTask: "به‌روزرسانی ماشین مجازی [{{ .name }}]"
VMDiskResizeTask: "افزایش ظرفیت دیسک [{{ .disk }}] ماشین مجازی [{{ .name }}]"
VMRenameTask: "تغییر نام ماشین مجازی [{{ .name }}] به [{{ .newName }}]"
VMDeleteTask: "حذف ماشین مجازی [{{ .name }}]"
VMTemplateCreateTask: "ذخیره ماشین مجازی [{{ .name }}] به عنوان قالب [{{ .template }}]"
VMTemplateDeleteTask: "حذف قالب ماشین مجازی [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm پیدا نشد، فقط فراداده ماشین مجازی [{{ .name }}] به اجبار حذف می‌شود"
VMForceDeleteMetadataAfterLibvirtFailed: "حذف ماشین مجازی [{{ .name }}] از libvirt ناموفق بود، فراداده به اجبار حذف می‌شود: {{ .err }}"
VMForceDeleteDiskFailed: "حذف اجباری فایل دیسک ماشین مجازی [{{ .path }}] ناموفق بود: {{ .err }}"
VMForceDeleteDiskSuccess: "فایل دیسک ماشین مجازی [{{ .path }}] به اجبار حذف شد"
VMSnapshotCreateTask: "ایجاد اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotCreateStep: "ایجاد اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotRecoverTask: "بازیابی اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotRecoverStep: "بازیابی اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotDeleteTask: "حذف اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotDeleteStep: "حذف اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
+51 -17
View File
@@ -649,6 +649,7 @@ CommonAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .msg }}。詳細
NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして'
LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして'
SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして'
CronJobSuccessAlert: 'パネル {{ .node }}{{ .ip }}: スケジュールタスク {{ .name }} が正常に完了しました。詳細はパネルにログインして確認してください。'
# ディスク
DeviceNotFound: 'デバイス {{ .name }} が見つかりません'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください'
ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください'
ErrDockerForwardPolicyDrop: 'このサーバーではネットワーク転送が制限されているため、現在コンテナのポート保護を有効にできません。'
Firewall: 'ファイアウォール'
FirewallTaskHost: 'ホストファイアウォール'
FirewallTaskForwarding: 'ポート転送'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'ファイアウォールルールを作成'
FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件'
FirewallCreateRuleSkipped: '未実行'
FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換'
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。実行済みのコマンドはロールバックしません'
FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド'
FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元'
FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期'
FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド'
FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元'
FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認'
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
FileTaskCopy: 'ファイルを {{ .dst }} にコピー'
FileTaskMove: 'ファイルを {{ .dst }} に移動'
FileTaskCompress: 'ファイルを {{ .dst }} に圧縮'
@@ -726,7 +713,54 @@ FileTaskDecompress: 'ファイルを {{ .dst }} に展開'
FileTaskSource: '元のパス:{{ .path }}'
FileTaskFormat: '圧縮形式:{{ .format }}'
FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
# 仮想マシン
ErrVMNotFound: "仮想マシンが存在しません"
ErrVMAlreadyExists: "仮想マシンは既に存在します"
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
ErrVMBridgeManaged: "ブリッジ名はすでに libvirt または Docker によって管理されています"
ErrVMNetworkInUse: "ネットワークは使用中のため削除できません"
ErrVMStorageInUse: "ストレージプールは使用中のため削除できません"
ErrVMStorageNameCannotChange: "ストレージプール名は変更できません"
ErrVMStoragePathCannotChange: "ストレージプールのパスは変更できません"
ErrVMRunning: "仮想マシンは実行中です"
ErrVMLibvirtConnect: "libvirt への接続に失敗しました: {{ .detail }}"
ErrVMPermission: "仮想マシンを操作する権限がありません: {{ .detail }}"
ErrVMNetworkNotFound: "仮想マシンネットワークが存在しません: {{ .detail }}"
ErrVMInvalidPath: "仮想マシンのパスが無効です: {{ .detail }}"
ErrVMInvalidInput: "仮想マシンのパラメータが無効です: {{ .detail }}"
ErrVMCommandFailed: "仮想マシンコマンドの実行に失敗しました: {{ .err }}"
ErrVMDiskResizeBlocked: "ディスクが拡張条件を満たしていないか、他のデバイスから参照されています。ディスクの状態を確認してください。"
ErrVMDiskReferenced: "この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。"
ErrVMDiskReferenceCheck: "仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。"
ErrVMSnapshotMetadataSync: "スナップショットは復元されましたが、ディスク情報の同期に失敗しました。更新して確認してください。再度復元する必要はありません。"
VMCreateTask: "仮想マシン [{{ .name }}] を作成"
VMCreateFromTemplateTask: "テンプレートから仮想マシン [{{ .name }}] を作成"
VMUpdateTask: "仮想マシン [{{ .name }}] を更新"
VMDiskResizeTask: "仮想マシン [{{ .name }}] のディスク [{{ .disk }}] を拡張"
VMRenameTask: "仮想マシン [{{ .name }}] を [{{ .newName }}] にリネーム"
VMDeleteTask: "仮想マシン [{{ .name }}] を削除"
VMTemplateCreateTask: "仮想マシン [{{ .name }}] をテンプレート [{{ .template }}] として保存"
VMTemplateDeleteTask: "仮想マシンテンプレート [{{ .name }}] を削除"
VMForceDeleteMetadataOnly: "1panel-kvm が見つかりません。仮想マシン [{{ .name }}] のメタデータのみを強制削除します"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt から仮想マシン [{{ .name }}] を削除できませんでした。メタデータを強制削除します: {{ .err }}"
VMForceDeleteDiskFailed: "仮想マシンディスクファイル [{{ .path }}] の強制削除に失敗しました: {{ .err }}"
VMForceDeleteDiskSuccess: "仮想マシンディスクファイル [{{ .path }}] を強制削除しました"
VMSnapshotCreateTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を作成"
VMSnapshotCreateStep: "仮想マシンのスナップショット [{{ .snapshot }}] を作成"
VMSnapshotRecoverTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を復元"
VMSnapshotRecoverStep: "仮想マシンのスナップショット [{{ .snapshot }}] を復元"
VMSnapshotDeleteTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を削除"
VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot }}] を削除"
+51 -17
View File
@@ -649,6 +649,7 @@ CommonAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .msg }}。자세한 내
NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
CronJobSuccessAlert: '패널 {{ .node }}{{ .ip }}: 예약 작업 {{ .name }}이(가) 성공적으로 완료되었습니다. 로그인하여 자세한 내용을 확인하십시오.'
# 디스크
DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요'
ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요'
ErrDockerForwardPolicyDrop: '현재 서버에서 네트워크 전달이 제한되어 컨테이너 포트 보호를 활성화할 수 없습니다.'
Firewall: '방화벽'
FirewallTaskHost: '호스트 방화벽'
FirewallTaskForwarding: '포트 포워딩'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: '방화벽 규칙 생성'
FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개'
FirewallCreateRuleSkipped: '미실행'
FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환'
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 실행된 명령은 롤백하지 않습니다'
FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩'
FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원'
FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화'
FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩'
FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원'
FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인'
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
FileTaskCopy: '{{ .dst }}에 파일 복사'
FileTaskMove: '{{ .dst }}로 파일 이동'
FileTaskCompress: '{{ .dst }}에 파일 압축'
@@ -726,7 +713,54 @@ FileTaskDecompress: '{{ .dst }}에 압축 해제'
FileTaskSource: '원본 경로: {{ .path }}'
FileTaskFormat: '압축 형식: {{ .format }}'
FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
# 가상 머신
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
ErrVMBridgeManaged: "브리지 이름이 이미 libvirt 또는 Docker에서 관리되고 있습니다"
ErrVMNetworkInUse: "네트워크가 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageInUse: "스토리지 풀이 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageNameCannotChange: "스토리지 풀 이름은 변경할 수 없습니다"
ErrVMStoragePathCannotChange: "스토리지 풀 경로는 변경할 수 없습니다"
ErrVMRunning: "가상 머신이 실행 중입니다"
ErrVMLibvirtConnect: "libvirt 연결 실패: {{ .detail }}"
ErrVMPermission: "가상 머신 작업 권한이 없습니다: {{ .detail }}"
ErrVMNetworkNotFound: "가상 머신 네트워크가 존재하지 않습니다: {{ .detail }}"
ErrVMInvalidPath: "가상 머신 경로가 유효하지 않습니다: {{ .detail }}"
ErrVMInvalidInput: "가상 머신 매개변수가 유효하지 않습니다: {{ .detail }}"
ErrVMCommandFailed: "가상 머신 명령 실행 실패: {{ .err }}"
ErrVMDiskResizeBlocked: "디스크가 확장 조건을 충족하지 않거나 다른 장치에서 참조하고 있습니다. 디스크 상태를 확인하세요."
ErrVMDiskReferenced: "다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다."
ErrVMDiskReferenceCheck: "가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요."
ErrVMSnapshotMetadataSync: "스냅샷은 복원되었지만 디스크 정보 동기화에 실패했습니다. 새로 고쳐 확인하세요. 스냅샷을 다시 복원할 필요는 없습니다."
VMCreateTask: "가상 머신 [{{ .name }}] 생성"
VMCreateFromTemplateTask: "템플릿에서 가상 머신 [{{ .name }}] 생성"
VMUpdateTask: "가상 머신 [{{ .name }}] 업데이트"
VMDiskResizeTask: "가상 머신 [{{ .name }}]의 디스크 [{{ .disk }}] 확장"
VMRenameTask: "가상 머신 [{{ .name }}]을(를) [{{ .newName }}](으)로 이름 변경"
VMDeleteTask: "가상 머신 [{{ .name }}] 삭제"
VMTemplateCreateTask: "가상 머신 [{{ .name }}]을(를) 템플릿 [{{ .template }}](으)로 저장"
VMTemplateDeleteTask: "가상 머신 템플릿 [{{ .name }}] 삭제"
VMForceDeleteMetadataOnly: "1panel-kvm을 찾을 수 없어 가상 머신 [{{ .name }}] 메타데이터만 강제 삭제합니다"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt에서 가상 머신 [{{ .name }}] 삭제 실패, 메타데이터 강제 삭제: {{ .err }}"
VMForceDeleteDiskFailed: "가상 머신 디스크 파일 [{{ .path }}] 강제 삭제 실패: {{ .err }}"
VMForceDeleteDiskSuccess: "가상 머신 디스크 파일 [{{ .path }}]을(를) 강제 삭제했습니다"
VMSnapshotCreateTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotCreateStep: "가상 머신 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotRecoverTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotRecoverStep: "가상 머신 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotDeleteTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 삭제"
VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제"
+51 -17
View File
@@ -639,6 +639,7 @@ CommonAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: {{ .msg }}. ເ
NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
CronJobSuccessAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ສຳເລັດແລ້ວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
#disk
DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}"
@@ -673,8 +674,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ'
ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່'
ErrDockerForwardPolicyDrop: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ຍັງບໍ່ສາມາດເປີດໃຊ້ການປ້ອງກັນພອດຄອນເທນເນີໄດ້ໃນຕອນນີ້.'
Firewall: 'ໄຟວອລ'
FirewallTaskHost: 'ໄຟວໍໂຮສ'
FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ'
@@ -688,28 +688,15 @@ FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ'
FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ'
FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ'
FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ'
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ຄຳສັ່ງທີ່ດຳເນີນການແລ້ວບໍ່ໄດ້ຖືກຍ້ອນກັບ'
FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}'
FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}'
FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ'
FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ'
FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ'
FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ'
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}'
@@ -717,7 +704,54 @@ FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}'
FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}'
FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ"
# virtual machine
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
ErrVMBridgeManaged: "ຊື່ບຣິດຈ໌ຖືກຈັດການໂດຍ libvirt ຫຼື Docker ແລ້ວ"
ErrVMNetworkInUse: "ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageInUse: "ພູນຈັດເກັບກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageNameCannotChange: "ບໍ່ສາມາດປ່ຽນຊື່ພູນຈັດເກັບໄດ້"
ErrVMStoragePathCannotChange: "ບໍ່ສາມາດປ່ຽນເສັ້ນທາງພູນຈັດເກັບໄດ້"
ErrVMRunning: "ເຄື່ອງສະເໝືອນກຳລັງເຮັດວຽກ"
ErrVMLibvirtConnect: "ເຊື່ອມຕໍ່ກັບ libvirt ບໍ່ສຳເລັດ: {{ .detail }}"
ErrVMPermission: "ບໍ່ມີສິດດຳເນີນການກັບເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMNetworkNotFound: "ບໍ່ມີເຄືອຂ່າຍເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMInvalidPath: "ເສັ້ນທາງເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMInvalidInput: "ພາລາມິເຕີເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMCommandFailed: "ຄຳສັ່ງເຄື່ອງສະເໝືອນລົ້ມເຫຼວ: {{ .err }}"
ErrVMDiskResizeBlocked: "ດິສກ໌ບໍ່ຕອບສະໜອງເງື່ອນໄຂການຂະຫຍາຍ ຫຼື ຖືກອ້າງອີງໂດຍອຸປະກອນອື່ນ. ກະລຸນາກວດສອບສະຖານະດິສກ໌."
ErrVMDiskReferenced: "ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ."
ErrVMDiskReferenceCheck: "ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ."
ErrVMSnapshotMetadataSync: "ກູ້ຄືນສະແນັບຊັອດແລ້ວ ແຕ່ຊິງຂໍ້ມູນດິສກ໌ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໃໝ່ ແລະ ກວດສອບ; ບໍ່ຈຳເປັນຕ້ອງກູ້ຄືນສະແນັບຊັອດອີກ."
VMCreateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMCreateFromTemplateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກແມ່ແບບ"
VMUpdateTask: "ອັບເດດເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMDiskResizeTask: "ຂະຫຍາຍດິສກ໌ [{{ .disk }}] ຂອງເຄື່ອງເສມືອນ [{{ .name }}]"
VMRenameTask: "ປ່ຽນຊື່ເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນ [{{ .newName }}]"
VMDeleteTask: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMTemplateCreateTask: "ບັນທຶກເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນແມ່ແບບ [{{ .template }}]"
VMTemplateDeleteTask: "ລຶບແມ່ແບບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataOnly: "ບໍ່ພົບ 1panel-kvm, ຈະບັງຄັບລຶບສະເພາະເມຕາດາຕາຂອງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກ libvirt ບໍ່ສຳເລັດ, ຈະບັງຄັບລຶບເມຕາດາຕາ: {{ .err }}"
VMForceDeleteDiskFailed: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ບໍ່ສຳເລັດ: {{ .err }}"
VMForceDeleteDiskSuccess: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ສຳເລັດແລ້ວ"
VMSnapshotCreateTask: "ສ້າງສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotCreateStep: "ສ້າງສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotRecoverTask: "ກູ້ຄືນສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotRecoverStep: "ກູ້ຄືນສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotDeleteTask: "ລຶບສະແນັບຊັອດ [{{ .snapshot }}] ຂອງເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
+51 -17
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.'
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.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: tugas berjadual {{ .name }} berjaya diselesaikan. Log masuk untuk melihat butiran.'
# cakera
DeviceNotFound: 'Peranti {{ .name }} tidak ditemui'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
ErrDockerNftablesChainUnavailable: 'Rantaian firewall IPv4 nftables Docker tidak ditemui. Pastikan versi Docker semasa menyokong bahagian belakang firewall nftables, mulakan semula Docker dan cuba lagi'
ErrDockerForwardPolicyDrop: 'Dasar lalai FORWARD {{ .family }} ialah DROP. Laraskan dasar dan cuba lagi'
ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi'
ErrDockerForwardPolicyDrop: 'Pelayan ini mengehadkan pemajuan rangkaian. Perlindungan port kontena tidak dapat diaktifkan buat masa ini.'
Firewall: 'Firewall'
FirewallTaskHost: 'Tembok api hos'
FirewallTaskForwarding: 'Pemajuan port'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Cipta peraturan tembok api'
FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal'
FirewallCreateRuleSkipped: 'Tidak dilaksanakan'
FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan'
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan'
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Arahan yang dilaksanakan tidak dibatalkan'
FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}'
FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}'
FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall'
FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port'
FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data'
FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
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"
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
FileTaskCopy: 'Salin fail ke {{ .dst }}'
FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
@@ -726,7 +713,54 @@ 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 }}"
ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada."
ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi."
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
# mesin maya
ErrVMNotFound: "Mesin maya tidak wujud"
ErrVMAlreadyExists: "Mesin maya sudah wujud"
ErrVMCountLimit: "Edisi Professional membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf kepada Enterprise dan cuba lagi."
ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}"
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm tidak ditemui"
ErrVMBridgeManaged: "Nama bridge telah diurus oleh libvirt atau Docker"
ErrVMNetworkInUse: "Rangkaian sedang digunakan dan tidak boleh dipadamkan"
ErrVMStorageInUse: "Kumpulan storan sedang digunakan dan tidak boleh dipadamkan"
ErrVMStorageNameCannotChange: "Nama kumpulan storan tidak boleh diubah"
ErrVMStoragePathCannotChange: "Laluan kumpulan storan tidak boleh diubah"
ErrVMRunning: "Mesin maya sedang berjalan"
ErrVMLibvirtConnect: "Gagal menyambung ke libvirt: {{ .detail }}"
ErrVMPermission: "Tiada kebenaran untuk mengendalikan mesin maya: {{ .detail }}"
ErrVMNetworkNotFound: "Rangkaian mesin maya tidak wujud: {{ .detail }}"
ErrVMInvalidPath: "Laluan mesin maya tidak sah: {{ .detail }}"
ErrVMInvalidInput: "Parameter mesin maya tidak sah: {{ .detail }}"
ErrVMCommandFailed: "Arahan mesin maya gagal: {{ .err }}"
ErrVMDiskResizeBlocked: "Cakera tidak memenuhi syarat pembesaran atau dirujuk oleh peranti lain. Semak status cakera."
ErrVMDiskReferenced: "Cakera maya ini digunakan oleh mesin maya atau peranti lain; pembesaran belum disokong."
ErrVMDiskReferenceCheck: "Rujukan cakera maya tidak dapat disahkan. Pembesaran tidak tersedia; semak cakera mesin maya lain."
ErrVMSnapshotMetadataSync: "Syot kilat telah dipulihkan, tetapi maklumat cakera gagal disegerakkan. Muat semula dan semak; tidak perlu memulihkan syot kilat sekali lagi."
VMCreateTask: "Cipta mesin maya [{{ .name }}]"
VMCreateFromTemplateTask: "Cipta mesin maya [{{ .name }}] daripada templat"
VMUpdateTask: "Kemas kini mesin maya [{{ .name }}]"
VMDiskResizeTask: "Besarkan cakera [{{ .disk }}] mesin maya [{{ .name }}]"
VMRenameTask: "Namakan semula mesin maya [{{ .name }}] kepada [{{ .newName }}]"
VMDeleteTask: "Padam mesin maya [{{ .name }}]"
VMTemplateCreateTask: "Simpan mesin maya [{{ .name }}] sebagai templat [{{ .template }}]"
VMTemplateDeleteTask: "Padam templat mesin maya [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm tidak ditemui, memadam metadata mesin maya [{{ .name }}] sahaja secara paksa"
VMForceDeleteMetadataAfterLibvirtFailed: "Gagal memadam mesin maya [{{ .name }}] daripada libvirt, memadam metadata secara paksa: {{ .err }}"
VMForceDeleteDiskFailed: "Gagal memadam fail cakera mesin maya [{{ .path }}] secara paksa: {{ .err }}"
VMForceDeleteDiskSuccess: "Fail cakera mesin maya [{{ .path }}] telah dipadam secara paksa"
VMSnapshotCreateTask: "Cipta syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotCreateStep: "Cipta syot kilat mesin maya [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Pulihkan syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotRecoverStep: "Pulihkan syot kilat mesin maya [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Padam syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotDeleteStep: "Padam syot kilat mesin maya [{{ .snapshot }}]"
+51 -17
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.'
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.'
CronJobSuccessAlert: 'Painel {{ .node }}{{ .ip }}: a tarefa agendada {{ .name }} foi concluída com sucesso. Faça login para ver os detalhes.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} não encontrado'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
ErrDockerNftablesChainUnavailable: 'A cadeia de firewall IPv4 do nftables do Docker não foi encontrada. Confirme se a versão atual do Docker oferece suporte ao backend de firewall nftables, reinicie o Docker e tente novamente'
ErrDockerForwardPolicyDrop: 'A política padrão de FORWARD para {{ .family }} é DROP. Ajuste-a e tente novamente'
ErrUFWRuleAdopt: 'Falha ao gerenciar a regra UFW: {{ .detail }}. Se a versão atual do UFW for anterior à 0.35, atualize o UFW e tente novamente'
ErrDockerForwardPolicyDrop: 'Este servidor restringe o encaminhamento de rede. Não é possível ativar a proteção de portas de contêineres no momento.'
Firewall: 'Firewall'
FirewallTaskHost: 'Firewall do host'
FirewallTaskForwarding: 'Encaminhamento de portas'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Criar regras de firewall'
FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas'
FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha'
FirewallCreateRuleSkipped: 'Não executada'
FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras'
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas'
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Os comandos executados não foram revertidos'
FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar regras do banco de dados para {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall'
FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas'
FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados'
FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas'
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
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"
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 }}'
@@ -726,7 +713,54 @@ 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 }}"
ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente."
ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente."
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
# máquina virtual
ErrVMNotFound: "Máquina virtual não existe"
ErrVMAlreadyExists: "Máquina virtual já existe"
ErrVMCountLimit: "A edição Professional permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para Enterprise e tente novamente."
ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}"
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm não encontrado"
ErrVMBridgeManaged: "O nome da bridge já é gerenciado pelo libvirt ou Docker"
ErrVMNetworkInUse: "A rede está em uso e não pode ser excluída"
ErrVMStorageInUse: "O pool de armazenamento está em uso e não pode ser excluído"
ErrVMStorageNameCannotChange: "O nome do pool de armazenamento não pode ser alterado"
ErrVMStoragePathCannotChange: "O caminho do pool de armazenamento não pode ser alterado"
ErrVMRunning: "Máquina virtual está em execução"
ErrVMLibvirtConnect: "Falha ao conectar ao libvirt: {{ .detail }}"
ErrVMPermission: "Sem permissão para operar a máquina virtual: {{ .detail }}"
ErrVMNetworkNotFound: "Rede da máquina virtual não existe: {{ .detail }}"
ErrVMInvalidPath: "Caminho da máquina virtual inválido: {{ .detail }}"
ErrVMInvalidInput: "Parâmetro da máquina virtual inválido: {{ .detail }}"
ErrVMCommandFailed: "Falha no comando da máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "O disco não atende aos requisitos de expansão ou é referenciado por outro dispositivo. Verifique seu estado."
ErrVMDiskReferenced: "Este disco virtual é usado por outra máquina virtual ou dispositivo e ainda não pode ser expandido."
ErrVMDiskReferenceCheck: "Não foi possível verificar as referências ao disco virtual. A expansão está indisponível; verifique os discos das outras máquinas virtuais."
ErrVMSnapshotMetadataSync: "O snapshot foi restaurado, mas a sincronização dos dados do disco falhou. Atualize e verifique; não restaure o snapshot novamente."
VMCreateTask: "Criar máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Criar máquina virtual [{{ .name }}] a partir do modelo"
VMUpdateTask: "Atualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Expandir disco [{{ .disk }}] da máquina virtual [{{ .name }}]"
VMRenameTask: "Renomear máquina virtual [{{ .name }}] para [{{ .newName }}]"
VMDeleteTask: "Excluir máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Salvar máquina virtual [{{ .name }}] como modelo [{{ .template }}]"
VMTemplateDeleteTask: "Excluir modelo de máquina virtual [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm não encontrado, excluindo à força apenas os metadados da máquina virtual [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Falha ao excluir a máquina virtual [{{ .name }}] do libvirt, excluindo metadados à força: {{ .err }}"
VMForceDeleteDiskFailed: "Falha ao excluir à força o arquivo de disco da máquina virtual [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Arquivo de disco da máquina virtual [{{ .path }}] excluído à força"
VMSnapshotCreateTask: "Criar snapshot [{{ .snapshot }}] para a máquina virtual [{{ .vm }}]"
VMSnapshotCreateStep: "Criar snapshot da máquina virtual [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Restaurar snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
VMSnapshotRecoverStep: "Restaurar snapshot da máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Excluir snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Excluir snapshot da máquina virtual [{{ .snapshot }}]"
+51 -17
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .msg }}. Войдит
NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.'
LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.'
SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.'
CronJobSuccessAlert: 'Панель {{ .node }}{{ .ip }}: запланированная задача {{ .name }} успешно завершена. Войдите для просмотра подробностей.'
# диск
DeviceNotFound: 'Устройство {{ .name }} не найдено'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку'
ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку'
ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку'
ErrDockerForwardPolicyDrop: 'На этом сервере ограничена пересылка сетевого трафика. Сейчас невозможно включить защиту портов контейнеров.'
Firewall: 'Межсетевой экран'
FirewallTaskHost: 'Межсетевой экран хоста'
FirewallTaskForwarding: 'Перенаправление портов'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Создание правил межсетевого
FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}'
FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}'
FirewallCreateRuleSkipped: 'Не выполнено'
FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил'
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Выполненные команды не отменены'
FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}'
FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}'
FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана'
FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов'
FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных'
FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики'
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
FileTaskCopy: 'Копирование файлов в {{ .dst }}'
FileTaskMove: 'Перемещение файлов в {{ .dst }}'
FileTaskCompress: 'Сжатие файлов в {{ .dst }}'
@@ -726,7 +713,54 @@ FileTaskDecompress: 'Распаковка файлов в {{ .dst }}'
FileTaskSource: 'Исходный путь: {{ .path }}'
FileTaskFormat: 'Формат архива: {{ .format }}'
FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу"
# виртуальная машина
ErrVMNotFound: "Виртуальная машина не существует"
ErrVMAlreadyExists: "Виртуальная машина уже существует"
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку."
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm не найден"
ErrVMBridgeManaged: "Имя моста уже управляется libvirt или Docker"
ErrVMNetworkInUse: "Сеть используется и не может быть удалена"
ErrVMStorageInUse: "Пул хранилища используется и не может быть удалён"
ErrVMStorageNameCannotChange: "Имя пула хранилища нельзя изменить"
ErrVMStoragePathCannotChange: "Путь к пулу хранилища нельзя изменить"
ErrVMRunning: "Виртуальная машина запущена"
ErrVMLibvirtConnect: "Не удалось подключиться к libvirt: {{ .detail }}"
ErrVMPermission: "Нет разрешения на операцию с виртуальной машиной: {{ .detail }}"
ErrVMNetworkNotFound: "Сеть виртуальной машины не существует: {{ .detail }}"
ErrVMInvalidPath: "Недопустимый путь виртуальной машины: {{ .detail }}"
ErrVMInvalidInput: "Недопустимые параметры виртуальной машины: {{ .detail }}"
ErrVMCommandFailed: "Ошибка выполнения команды виртуальной машины: {{ .err }}"
ErrVMDiskResizeBlocked: "Диск не соответствует условиям расширения или используется другим устройством. Проверьте состояние диска."
ErrVMDiskReferenced: "Этот виртуальный диск используется другой виртуальной машиной или устройством; расширение пока недоступно."
ErrVMDiskReferenceCheck: "Не удалось проверить ссылки на виртуальный диск. Расширение недоступно; проверьте диски других виртуальных машин."
ErrVMSnapshotMetadataSync: "Снимок восстановлен, но синхронизация данных диска не удалась. Обновите данные и проверьте их; повторно восстанавливать снимок не нужно."
VMCreateTask: "Создать виртуальную машину [{{ .name }}]"
VMCreateFromTemplateTask: "Создать виртуальную машину [{{ .name }}] из шаблона"
VMUpdateTask: "Обновить виртуальную машину [{{ .name }}]"
VMDiskResizeTask: "Расширить диск [{{ .disk }}] виртуальной машины [{{ .name }}]"
VMRenameTask: "Переименовать виртуальную машину [{{ .name }}] в [{{ .newName }}]"
VMDeleteTask: "Удалить виртуальную машину [{{ .name }}]"
VMTemplateCreateTask: "Сохранить виртуальную машину [{{ .name }}] как шаблон [{{ .template }}]"
VMTemplateDeleteTask: "Удалить шаблон виртуальной машины [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm не найден, принудительно удаляются только метаданные виртуальной машины [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Не удалось удалить виртуальную машину [{{ .name }}] из libvirt, принудительно удаляются метаданные: {{ .err }}"
VMForceDeleteDiskFailed: "Не удалось принудительно удалить файл диска виртуальной машины [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Файл диска виртуальной машины [{{ .path }}] принудительно удален"
VMSnapshotCreateTask: "Создать снимок [{{ .snapshot }}] для виртуальной машины [{{ .vm }}]"
VMSnapshotCreateStep: "Создать снимок виртуальной машины [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Восстановить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
VMSnapshotRecoverStep: "Восстановить снимок виртуальной машины [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Удалить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
VMSnapshotDeleteStep: "Удалить снимок виртуальной машины [{{ .snapshot }}]"
+51 -17
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.'
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.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: zamanlanmış görev {{ .name }} başarıyla tamamlandı. Ayrıntıları görmek için giriş yapın.'
# disk
DeviceNotFound: 'Cihaz {{ .name }} bulunamadı'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 güvenlik duvarı zinciri bulunamadı. Geçerli Docker sürümünün nftables güvenlik duvarı arka ucunu desteklediğini doğrulayın, Docker’ı yeniden başlatıp tekrar deneyin'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD varsayılan ilkesi DROP. İlkeyi düzenleyip tekrar deneyin'
ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35’ten eskiyse UFW’yi yükseltip tekrar deneyin'
ErrDockerForwardPolicyDrop: 'Bu sunucu ağ yönlendirmesini kısıtlıyor. Konteyner port koruması şu anda etkinleştirilemiyor.'
Firewall: 'Güvenlik duvarı'
FirewallTaskHost: 'Ana makine güvenlik duvarı'
FirewallTaskForwarding: 'Port yönlendirme'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: 'Güvenlik duvarı kuralları oluştur'
FirewallCreateRulesResult: 'Oluşturma sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız'
FirewallCreateRuleSkipped: 'Yürütülmedi'
FirewallCreateBatchStep: '{{ .backend }} için {{ .count }} kuralı toplu gönder'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; {{ .count }} kurala dönüştürüldü'
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi'
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Yürütülen komutlar geri alınmadı'
FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla'
FirewallRestoreRulesStep: 'Veritabanı kurallarını {{ .name }} üzerine geri yükle'
FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle'
FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla'
FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle'
FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle'
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
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"
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'
@@ -726,7 +713,54 @@ 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 }}"
ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin."
ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin."
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
# sanal makine
ErrVMNotFound: "Sanal makine mevcut değil"
ErrVMAlreadyExists: "Sanal makine zaten mevcut"
ErrVMCountLimit: "Professional sürümü en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya Enterprise sürümüne yükseltip tekrar deneyin."
ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}"
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
ErrVMHelperNotFound: "1panel-kvm bulunamadı"
ErrVMBridgeManaged: "Bridge adı zaten libvirt veya Docker tarafından yönetiliyor"
ErrVMNetworkInUse: "Ağ kullanımda ve silinemez"
ErrVMStorageInUse: "Depolama havuzu kullanımda ve silinemez"
ErrVMStorageNameCannotChange: "Depolama havuzunun adı değiştirilemez"
ErrVMStoragePathCannotChange: "Depolama havuzunun yolu değiştirilemez"
ErrVMRunning: "Sanal makine çalışıyor"
ErrVMLibvirtConnect: "libvirt bağlantısı başarısız: {{ .detail }}"
ErrVMPermission: "Sanal makine işlemi için izin yok: {{ .detail }}"
ErrVMNetworkNotFound: "Sanal makine ağı mevcut değil: {{ .detail }}"
ErrVMInvalidPath: "Geçersiz sanal makine yolu: {{ .detail }}"
ErrVMInvalidInput: "Geçersiz sanal makine parametresi: {{ .detail }}"
ErrVMCommandFailed: "Sanal makine komutu başarısız: {{ .err }}"
ErrVMDiskResizeBlocked: "Disk genişletme koşullarını karşılamıyor veya başka bir aygıt tarafından kullanılıyor. Disk durumunu kontrol edin."
ErrVMDiskReferenced: "Bu sanal disk başka bir sanal makine veya aygıt tarafından kullanılıyor; genişletme henüz desteklenmiyor."
ErrVMDiskReferenceCheck: "Sanal disk başvuruları doğrulanamadı. Genişletme kullanılamıyor; diğer sanal makinelerin disklerini kontrol edin."
ErrVMSnapshotMetadataSync: "Anlık görüntü geri yüklendi ancak disk bilgileri eşitlenemedi. Yenileyip kontrol edin; anlık görüntüyü tekrar geri yüklemeyin."
VMCreateTask: "Sanal makine [{{ .name }}] oluştur"
VMCreateFromTemplateTask: "Şablondan sanal makine [{{ .name }}] oluştur"
VMUpdateTask: "Sanal makine [{{ .name }}] güncelle"
VMDiskResizeTask: "[{{ .name }}] sanal makinesinin [{{ .disk }}] diskini genişlet"
VMRenameTask: "Sanal makine [{{ .name }}] adını [{{ .newName }}] olarak değiştir"
VMDeleteTask: "Sanal makine [{{ .name }}] sil"
VMTemplateCreateTask: "Sanal makine [{{ .name }}] öğesini şablon [{{ .template }}] olarak kaydet"
VMTemplateDeleteTask: "Sanal makine şablonu [{{ .name }}] sil"
VMForceDeleteMetadataOnly: "1panel-kvm bulunamadı, yalnızca sanal makine [{{ .name }}] meta verileri zorla siliniyor"
VMForceDeleteMetadataAfterLibvirtFailed: "Sanal makine [{{ .name }}] libvirt üzerinden silinemedi, meta veriler zorla siliniyor: {{ .err }}"
VMForceDeleteDiskFailed: "Sanal makine disk dosyası [{{ .path }}] zorla silinemedi: {{ .err }}"
VMForceDeleteDiskSuccess: "Sanal makine disk dosyası [{{ .path }}] zorla silindi"
VMSnapshotCreateTask: "Sanal makine [{{ .vm }}] için anlık görüntü [{{ .snapshot }}] oluştur"
VMSnapshotCreateStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] oluştur"
VMSnapshotRecoverTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] geri yükle"
VMSnapshotRecoverStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] geri yükle"
VMSnapshotDeleteTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] sil"
VMSnapshotDeleteStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] sil"
+51 -17
View File
@@ -649,6 +649,7 @@ CommonAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},詳情請登入
NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。'
LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。'
SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。'
CronJobSuccessAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務 {{ .name }} 執行成功,詳情請登入面板檢視。'
# 磁碟
DeviceNotFound: '裝置 {{ .name }} 未找到'
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: '火山方舟 Coding Plan'
# 防火牆
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試'
ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試'
ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試'
ErrDockerForwardPolicyDrop: '目前伺服器限制了網路轉送,暫時無法啟用容器連接埠防護。'
Firewall: '防火牆'
FirewallTaskHost: '主機防火牆'
FirewallTaskForwarding: '連接埠轉發'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: '建立防火牆規則'
FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條'
FirewallCreateRuleSkipped: '未執行'
FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則'
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
FirewallCreateRuleExecutionFailed: '規則建立失敗,已執行的命令不回復'
FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈'
FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}'
FirewallSyncWhitelistStep: '同步防火牆連接埠白名單'
FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈'
FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則'
FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
FileTaskCopy: '複製檔案至 {{ .dst }}'
FileTaskMove: '移動檔案至 {{ .dst }}'
FileTaskCompress: '壓縮檔案至 {{ .dst }}'
@@ -726,7 +713,54 @@ FileTaskDecompress: '解壓檔案至 {{ .dst }}'
FileTaskSource: '來源路徑:{{ .path }}'
FileTaskFormat: '壓縮格式:{{ .format }}'
FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
# 虛擬機
ErrVMNotFound: "虛擬機不存在"
ErrVMAlreadyExists: "虛擬機已存在"
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在"
ErrVMBridgeManaged: "橋接名稱已被 libvirt 或 Docker 管理"
ErrVMNetworkInUse: "網路正在被使用,無法刪除"
ErrVMStorageInUse: "儲存池正在被使用,無法刪除"
ErrVMStorageNameCannotChange: "儲存池名稱無法修改"
ErrVMStoragePathCannotChange: "儲存池路徑無法修改"
ErrVMRunning: "虛擬機正在執行"
ErrVMLibvirtConnect: "連接 libvirt 失敗: {{ .detail }}"
ErrVMPermission: "沒有虛擬機操作權限: {{ .detail }}"
ErrVMNetworkNotFound: "虛擬機網路不存在: {{ .detail }}"
ErrVMInvalidPath: "虛擬機路徑非法: {{ .detail }}"
ErrVMInvalidInput: "虛擬機參數非法: {{ .detail }}"
ErrVMCommandFailed: "虛擬機指令執行失敗: {{ .err }}"
ErrVMDiskResizeBlocked: "磁碟不符合擴容條件,或被其他裝置引用,請檢查磁碟狀態"
ErrVMDiskReferenced: "此虛擬硬碟被其他虛擬機或裝置使用,暫不支援擴容"
ErrVMDiskReferenceCheck: "無法完成虛擬硬碟引用檢查,暫不支援擴容,請檢查其他虛擬機的磁碟狀態"
ErrVMSnapshotMetadataSync: "快照已還原,但磁碟資訊同步失敗,請重新整理檢查,無需重複還原快照"
VMCreateTask: "建立虛擬機 [{{ .name }}]"
VMCreateFromTemplateTask: "從範本建立虛擬機 [{{ .name }}]"
VMUpdateTask: "更新虛擬機 [{{ .name }}]"
VMDiskResizeTask: "擴容虛擬機 [{{ .name }}] 磁碟 [{{ .disk }}]"
VMRenameTask: "重新命名虛擬機 [{{ .name }}] 為 [{{ .newName }}]"
VMDeleteTask: "刪除虛擬機 [{{ .name }}]"
VMTemplateCreateTask: "轉存虛擬機 [{{ .name }}] 為範本 [{{ .template }}]"
VMTemplateDeleteTask: "刪除虛擬機範本 [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm 不存在,僅強制刪除虛擬機 [{{ .name }}] 元資料"
VMForceDeleteMetadataAfterLibvirtFailed: "從 libvirt 刪除虛擬機 [{{ .name }}] 失敗,強制刪除元資料: {{ .err }}"
VMForceDeleteDiskFailed: "強制刪除虛擬機磁碟檔案 [{{ .path }}] 失敗: {{ .err }}"
VMForceDeleteDiskSuccess: "已強制刪除虛擬機磁碟檔案 [{{ .path }}]"
VMSnapshotCreateTask: "建立虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotCreateStep: "建立虛擬機快照 [{{ .snapshot }}]"
VMSnapshotRecoverTask: "復原虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "復原虛擬機快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "刪除虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]"
+51 -17
View File
@@ -649,6 +649,7 @@ CommonAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},请登录面板
NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。"
LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。"
SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。"
CronJobSuccessAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行成功,请登录面板查看详情。"
# 磁盘
DeviceNotFound: "设备 {{ .name }} 未找到"
@@ -682,8 +683,7 @@ AIProviderArkCodingPlan: "火山方舟 Coding Plan"
# 防火墙
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试"
ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试'
ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试"
ErrDockerForwardPolicyDrop: '当前服务器限制了网络转发,暂时无法启用容器端口防护。'
Firewall: "防火墙"
FirewallTaskHost: '主机防火墙'
FirewallTaskForwarding: '端口转发'
@@ -697,28 +697,15 @@ FirewallCreateRulesStep: '创建防火墙规则'
FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条'
FirewallCreateRuleSkipped: '未执行'
FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则'
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
FirewallCreateRuleExecutionFailed: '规则创建失败,已执行的命令不回滚'
FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链"
FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}"
FirewallSyncWhitelistStep: "同步防火墙端口白名单"
FirewallEnableForwardingStep: "启用并绑定端口转发链"
FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则"
FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
FileTaskCopy: '复制文件到 {{ .dst }}'
FileTaskMove: '移动文件到 {{ .dst }}'
FileTaskCompress: '压缩文件到 {{ .dst }}'
@@ -726,7 +713,54 @@ FileTaskDecompress: '解压文件到 {{ .dst }}'
FileTaskSource: '源路径:{{ .path }}'
FileTaskFormat: '压缩格式:{{ .format }}'
FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
# 虚拟机
ErrVMNotFound: "虚拟机不存在"
ErrVMAlreadyExists: "虚拟机已存在"
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在"
ErrVMBridgeManaged: "桥接名称已被 libvirt 或 Docker 管理"
ErrVMNetworkInUse: "网络正在被使用,无法删除"
ErrVMStorageInUse: "存储池正在被使用,无法删除"
ErrVMStorageNameCannotChange: "存储池名称无法修改"
ErrVMStoragePathCannotChange: "存储池路径无法修改"
ErrVMRunning: "虚拟机正在运行"
ErrVMLibvirtConnect: "连接 libvirt 失败: {{ .detail }}"
ErrVMPermission: "没有虚拟机操作权限: {{ .detail }}"
ErrVMNetworkNotFound: "虚拟机网络不存在: {{ .detail }}"
ErrVMInvalidPath: "虚拟机路径非法: {{ .detail }}"
ErrVMInvalidInput: "虚拟机参数非法: {{ .detail }}"
ErrVMCommandFailed: "虚拟机命令执行失败: {{ .err }}"
ErrVMDiskResizeBlocked: "磁盘不符合扩容条件,或被其他设备引用,请检查磁盘状态"
ErrVMDiskReferenced: "该虚拟硬盘被其他虚拟机或设备使用,暂不支持扩容"
ErrVMDiskReferenceCheck: "无法完成虚拟硬盘引用检查,暂不支持扩容,请检查其他虚拟机的磁盘状态"
ErrVMSnapshotMetadataSync: "快照已恢复,但磁盘信息同步失败,请刷新检查,无需重复恢复快照"
VMCreateTask: "创建虚拟机 [{{ .name }}]"
VMCreateFromTemplateTask: "从模板创建虚拟机 [{{ .name }}]"
VMUpdateTask: "更新虚拟机 [{{ .name }}]"
VMDiskResizeTask: "扩容虚拟机 [{{ .name }}] 磁盘 [{{ .disk }}]"
VMRenameTask: "重命名虚拟机 [{{ .name }}] 为 [{{ .newName }}]"
VMDeleteTask: "删除虚拟机 [{{ .name }}]"
VMTemplateCreateTask: "转存虚拟机 [{{ .name }}] 为模板 [{{ .template }}]"
VMTemplateDeleteTask: "删除虚拟机模板 [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm 不存在,仅强制删除虚拟机 [{{ .name }}] 元数据"
VMForceDeleteMetadataAfterLibvirtFailed: "从 libvirt 删除虚拟机 [{{ .name }}] 失败,强制删除元数据: {{ .err }}"
VMForceDeleteDiskFailed: "强制删除虚拟机磁盘文件 [{{ .path }}] 失败: {{ .err }}"
VMForceDeleteDiskSuccess: "已强制删除虚拟机磁盘文件 [{{ .path }}]"
VMSnapshotCreateTask: "创建虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotCreateStep: "创建虚拟机快照 [{{ .snapshot }}]"
VMSnapshotRecoverTask: "恢复虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "恢复虚拟机快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "删除虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "删除虚拟机快照 [{{ .snapshot }}]"
+1
View File
@@ -13,6 +13,7 @@ func Init() {
global.TaskDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "task.db"), "task")
global.MonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "monitor.db"), "monitor")
global.GPUMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "gpu_monitor.db"), "gpu_monitor")
global.VLLMMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "vllm_monitor.db"), "vllm_monitor")
global.AlertDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "alert.db"), "alert")
if _, err := os.Stat(path.Join(global.Dir.DbDir, "core.db")); err == nil {
+23 -22
View File
@@ -6,20 +6,26 @@ import (
"os"
"time"
"errors"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
"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/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm"
)
func Init() {
ctx := context.Background()
defer initDockerPortGuard(ctx)
if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err)
}
client, err := service.NewSelectedSystemFirewallClient()
if err != nil {
global.LOG.Errorf("select system firewall provider failed, err: %v", err)
@@ -31,38 +37,33 @@ func Init() {
if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil {
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
}
if initialize {
initDockerPortGuard(ctx)
}
}()
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
families := []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
nftFamilies := []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
ipv6Status, err := repo.NewISettingRepo().GetValueByKey(constant.FirewallIPv6SupportKey)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
global.LOG.Errorf("load firewall IPv6 setting failed: %v", err)
return
}
if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil {
global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err)
}
if err := migrationutils.TransferFirewallForwarding(ctx); err != nil {
global.LOG.Errorf("transfer legacy forwarding rules failed, err: %v", err)
return
}
if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
if ipv6Status == constant.StatusDisable {
global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chains and saved rules during startup")
families = families[:1]
nftFamilies = nftFamilies[:1]
}
initialize = needInit()
if !initialize {
repairIptablesBaseChains(clientName)
repairIptablesBaseChains(clientName, families...)
return
}
InitPingStatus()
global.LOG.Info("initializing firewall settings...")
if clientName == "nftables" {
if err := nftables_helper.Restore(); err != nil {
if err := nftables_helper.Restore(nftFamilies...); err != nil {
global.LOG.Errorf("restore nftables rules failed, err: %v", err)
}
status, _ := repo.NewISettingRepo().GetValueByKey("IptablesStatus")
if status == constant.StatusEnable {
if err := nftables_helper.Bind(); err != nil {
if err := nftables_helper.Bind(nftFamilies...); err != nil {
global.LOG.Errorf("bind nftables base chains failed, err: %v", err)
}
}
@@ -78,7 +79,7 @@ func Init() {
global.LOG.Errorf("load required firewall ports failed, err: %v", err)
return
}
if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
if err := iptables_helper.RestoreBaseChains(requiredPorts, families...); err != nil {
global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return
}
@@ -94,7 +95,7 @@ func Init() {
}
func repairIptablesBaseChains(clientName string) {
func repairIptablesBaseChains(clientName string, families ...string) {
if clientName != constant.FirewallProviderIptables {
return
}
@@ -108,7 +109,7 @@ func repairIptablesBaseChains(clientName string) {
global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err)
return
}
if err := iptables_helper.RepairBaseChains(ports); err != nil {
if err := iptables_helper.RepairBaseChains(ports, families...); err != nil {
global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
}
}
@@ -120,7 +121,7 @@ func initDockerPortGuard(ctx context.Context) {
)
var restoreErr error
for attempt := 1; attempt <= attempts; attempt++ {
restoreErr = service.ReconcileDockerPortGuard(ctx)
restoreErr = service.RestoreDockerPortGuard(ctx)
if restoreErr == nil {
return
}
+33 -1
View File
@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -13,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/alert_push"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
func Init() {
@@ -174,6 +176,36 @@ func initAlertTask() {
}
func initMonitorDB() {
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorGPU{}, &model.MonitorIO{})
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorIO{})
_ = global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
_ = global.TaskDB.AutoMigrate(&model.Task{})
// building indexes on large monitor tables can take seconds, keep it off the startup path;
// WAL mode leaves readers unblocked and busy_timeout covers the collector's inserts meanwhile
go ensureMonitorIndexes()
}
func ensureMonitorIndexes() {
indexes := []struct {
db *gorm.DB
stmt string
}{
// created_at alone serves unfiltered range queries and retention cleanup
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_bases_created ON monitor_bases(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_created ON monitor_ios(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_created ON monitor_networks(created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_created ON monitor_gpus(created_at)"},
// (name, created_at) serves per-device range queries and distinct name lookups
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_name_created ON monitor_ios(name, created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_name_created ON monitor_networks(name, created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_product_created ON monitor_gpus(product_name, created_at)"},
}
start := time.Now()
for _, index := range indexes {
if err := index.db.Exec(index.stmt).Error; err != nil {
global.LOG.Warnf("create monitor index failed, stmt: %s, err: %v", index.stmt, err)
}
}
if elapsed := time.Since(start); elapsed > time.Second {
global.LOG.Infof("monitor indexes ready, took %s", elapsed)
}
}
+4 -2
View File
@@ -108,9 +108,11 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddFirewallRuleTable,
migrations.InitDockerPortGuardStatus,
migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallDescriptions,
migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
}
}
+29 -63
View File
@@ -59,7 +59,6 @@ var AddTable = &gormigrate.Migration{
&model.Favorite{},
&model.FileShare{},
&model.Host{},
&model.FirewallRule{},
&model.Ftp{},
&model.ImageRepo{},
&model.ScriptLibrary{},
@@ -1784,7 +1783,7 @@ var AddComposePinned = &gormigrate.Migration{
var AddFirewallRuleTable = &gormigrate.Migration{
ID: "20260819-add-firewall-v2-tables",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.FirewallRule{}, &model.DockerPortGuardPolicy{}, &model.ForwardingRule{})
return tx.AutoMigrate(&model.CommonDescription{})
},
}
@@ -1837,74 +1836,41 @@ var NormalizeFirewallBackendSelections = &gormigrate.Migration{
},
}
var SimplifyFirewallRulePolicy = &gormigrate.Migration{
ID: "20260826-simplify-firewall-rule-policy",
var MigrateFirewallDescriptions = &gormigrate.Migration{
ID: "20260921-migrate-firewall-descriptions",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.FirewallRule{}) {
return nil
}
return tx.Transaction(func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn("firewall_rules", "compatibility_error") {
if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN compatibility_error text NOT NULL DEFAULT ''").Error; err != nil {
return err
}
}
if !tx.Migrator().HasColumn("firewall_rules", "sequence") {
if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN sequence integer").Error; err != nil {
return err
}
}
if tx.Migrator().HasColumn("firewall_rules", "native_kind") {
if err := tx.Exec(`UPDATE firewall_rules
SET compatibility_error = 'legacy native firewall rule requires manual recreation: ' || native_kind
WHERE native_kind IN ('zone_service', 'ufw_application', 'opaque')`).Error; err != nil {
return err
}
}
if tx.Migrator().HasColumn("firewall_rules", "provider") {
if err := tx.Exec(`UPDATE firewall_rules
SET priority = NULL, sequence = NULL`).Error; err != nil {
return err
}
}
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_rules_sequence ON firewall_rules(sequence)").Error; err != nil {
return err
}
for _, index := range []string{
"idx_firewall_rules_scope_key",
"uk_firewall_rules_scope_rule",
"uk_firewall_rules_scope_match",
} {
if err := tx.Exec("DROP INDEX IF EXISTS " + index).Error; err != nil {
return err
}
}
for _, column := range []string{
"provider", "scope_key", "location", "native_kind", "order_index", "order_bucket", "rule_key", "match_key",
} {
if tx.Migrator().HasColumn("firewall_rules", column) {
_ = tx.Exec("ALTER TABLE firewall_rules DROP COLUMN " + column).Error
}
}
return nil
})
return tx.Transaction(migrationutils.MigrateHostFirewallDescriptions)
},
}
var AddDockerPortGuardReadOnly = &gormigrate.Migration{
ID: "20260902-add-docker-port-guard-read-only",
var AddAcceleratorMetrics = &gormigrate.Migration{
ID: "20260928-accelerator-vendor-metrics",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.DockerPortGuardPolicy{}) {
return nil
}
if err := tx.AutoMigrate(&model.DockerPortGuardPolicy{}); err != nil {
return err
}
if tx.Migrator().HasIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") {
if err := tx.Migrator().DropIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint"); err != nil {
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
var AddVLLMMonitor = &gormigrate.Migration{
ID: "20260928-vllm-monitor",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddMonitorSettings = &gormigrate.Migration{
ID: "20260929-monitor-settings",
Migrate: func(tx *gorm.DB) error {
for _, key := range []string{"MonitorStatus", "MonitorInterval", "MonitorStoreDays"} {
var setting model.Setting
if err := tx.Where("key = ?", key).First(&setting).Error; err != nil {
return err
}
for _, prefix := range []string{"GPU", "VLLM"} {
if err := tx.Where("key = ?", prefix+key).FirstOrCreate(&model.Setting{Key: prefix + key, Value: setting.Value}).Error; err != nil {
return err
}
}
}
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
return nil
},
}
@@ -1,373 +0,0 @@
package utils
import (
"context"
"errors"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const firewallTransferMigrationID = "firewall-transfer"
type legacyFirewalldForward struct {
rule forwarding.Rule
spec string
}
type legacyFirewalldForwardFailure struct {
spec string
err error
}
type firewallTransferSource struct {
rules []forwarding.Rule
firewalld []legacyFirewalldForward
provider string
cleanupOld func([]legacyFirewalldForward) error
}
type firewallTransfer struct {
db *gorm.DB
load func() (firewallTransferSource, error)
}
// TransferFirewallForwarding migrates the legacy system-backed forwarding
// inventory into the forwarding_rules table once. The migrations table is
// also used as the completion ledger so a failed transfer can be retried on
// the next agent start without introducing another persisted setting.
func TransferFirewallForwarding(ctx context.Context) error {
transfer := &firewallTransfer{
db: global.DB,
load: loadLegacyFirewallForwarding,
}
return transfer.run(ctx)
}
func (t *firewallTransfer) run(ctx context.Context) error {
if t.db == nil {
return errors.New("firewall transfer database is required")
}
completed, err := firewallTransferCompleted(t.db)
if err != nil {
return err
}
if completed {
return nil
}
if t.load == nil {
return errors.New("legacy firewall forwarding loader is required")
}
source, err := t.load()
if err != nil {
return err
}
if err := importLegacyForwardingRules(ctx, t.db, source.rules, source.provider); err != nil {
return err
}
if len(source.firewalld) > 0 {
if source.cleanupOld == nil {
return errors.New("legacy firewalld cleanup is required")
}
if err := source.cleanupOld(source.firewalld); err != nil {
return err
}
}
return markFirewallTransferCompleted(t.db)
}
func firewallTransferCompleted(db *gorm.DB) (bool, error) {
return migrationRecordExists(db, firewallTransferMigrationID)
}
func markFirewallTransferCompleted(db *gorm.DB) error {
return markMigrationRecord(db, firewallTransferMigrationID)
}
func migrationRecordExists(db *gorm.DB, migrationID string) (bool, error) {
var count int64
if err := db.Table("migrations").Where("id = ?", migrationID).Count(&count).Error; err != nil {
return false, fmt.Errorf("check migration record %q: %w", migrationID, err)
}
return count > 0, nil
}
func markMigrationRecord(db *gorm.DB, migrationID string) error {
if err := db.Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
Create(map[string]interface{}{"id": migrationID}).Error; err != nil {
return fmt.Errorf("record migration %q: %w", migrationID, err)
}
return nil
}
func importLegacyForwardingRules(ctx context.Context, db *gorm.DB, rules []forwarding.Rule, provider string) error {
models := make([]model.ForwardingRule, 0, len(rules))
seen := make(map[string]struct{}, len(rules))
for _, rule := range rules {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return fmt.Errorf("normalize legacy forwarding rule: %w", err)
}
key := normalized.Identity()
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
models = append(models, model.ForwardingRule{
Family: normalized.Family, Protocol: normalized.Protocol, Port: normalized.Port,
TargetIP: normalized.TargetIP, TargetPort: normalized.TargetPort, Interface: normalized.Interface,
})
}
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if len(models) > 0 {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error; err != nil {
return fmt.Errorf("import legacy forwarding rules: %w", err)
}
if err := updateOrCreateSetting(tx, "IptablesForwardStatus", constant.StatusEnable); err != nil {
return err
}
if provider != "" {
if err := updateOrCreateSetting(tx, "ForwardingBackend", provider); err != nil {
return err
}
}
}
return nil
})
}
func updateOrCreateSetting(tx *gorm.DB, key, value string) error {
result := tx.Model(&model.Setting{}).Where("key = ?", key).Update("value", value)
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
return nil
}
return tx.Create(&model.Setting{Key: key, Value: value}).Error
}
func loadLegacyFirewallForwarding() (firewallTransferSource, error) {
source := firewallTransferSource{cleanupOld: cleanupLegacyFirewalldForwarding}
if cmd.Which("firewall-cmd") {
firewalldRules, err := listLegacyFirewalldForwarding()
if err != nil {
return firewallTransferSource{}, err
}
supportedRules, cleanupRules, failures := selectSupportedLegacyFirewalldForwarding(firewalldRules)
for _, failure := range failures {
if global.LOG != nil {
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
}
}
source.rules = append(source.rules, supportedRules...)
source.firewalld = append(source.firewalld, cleanupRules...)
if len(source.rules) > 0 {
source.provider = "iptables"
}
return source, nil
}
if _, err := lifecycle.ResolveIptablesCommands(); err != nil {
return source, nil
}
rules, err := listLegacyIptablesForwarding()
if err != nil {
return firewallTransferSource{}, err
}
source.rules = append(source.rules, rules...)
return source, nil
}
func selectSupportedLegacyFirewalldForwarding(items []legacyFirewalldForward) (
[]forwarding.Rule, []legacyFirewalldForward, []legacyFirewalldForwardFailure,
) {
rules := make([]forwarding.Rule, 0, len(items))
cleanup := make([]legacyFirewalldForward, 0, len(items))
failures := make([]legacyFirewalldForwardFailure, 0)
for _, item := range items {
if _, err := forwarding.NormalizeRule(item.rule); err != nil {
failures = append(failures, legacyFirewalldForwardFailure{spec: item.spec, err: err})
continue
}
rules = append(rules, item.rule)
cleanup = append(cleanup, item)
}
return rules, cleanup, failures
}
func listLegacyIptablesForwarding() ([]forwarding.Rule, error) {
exists, err := iptables_helper.CheckChainExist(iptables_helper.NatTab, forwarding.ChainPreRouting)
if err != nil {
return nil, err
}
if !exists {
return nil, nil
}
stdout, err := iptables_helper.RunWithStd(
iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers",
)
if err != nil {
return nil, fmt.Errorf("list legacy iptables forwarding rules: %w", err)
}
return parseLegacyIptablesForwarding(stdout), nil
}
func parseLegacyIptablesForwarding(stdout string) []forwarding.Rule {
rules := make([]forwarding.Rule, 0)
for _, line := range strings.Split(stdout, "\n") {
fields := strings.Fields(line)
if len(fields) < 13 {
continue
}
rule := forwarding.Rule{
Family: forwarding.FamilyIPv4, Protocol: loadLegacyIptablesProtocol(fields[4]),
Interface: fields[6], Port: loadLegacyIptablesSourcePort(fields[11]),
}
if len(fields) == 15 && fields[13] == "ports" {
rule.TargetPort = fields[14]
}
if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") {
target := strings.TrimPrefix(fields[12], "to:")
separator := strings.LastIndex(target, ":")
if separator > 0 {
rule.TargetIP, rule.TargetPort = target[:separator], target[separator+1:]
}
}
if rule.TargetIP == "" {
rule.TargetIP = "127.0.0.1"
}
rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":")
rules = append(rules, rule)
}
return rules
}
func loadLegacyIptablesProtocol(protocol string) string {
switch protocol {
case "6":
return "tcp"
case "17":
return "udp"
default:
return protocol
}
}
func loadLegacyIptablesSourcePort(value string) string {
port := ""
if strings.Contains(value, "dpt:") {
port = strings.ReplaceAll(value, "dpt:", "")
}
if strings.Contains(value, "dpts:") {
port = strings.ReplaceAll(value, "dpts:", "")
}
return strings.ReplaceAll(port, ":", "-")
}
func listLegacyFirewalldForwarding() ([]legacyFirewalldForward, error) {
stdout, err := cmd.NewCommandMgr().RunWithStdout(
"firewall-cmd", "--permanent", "--zone=public", "--list-forward-ports",
)
if err != nil {
return nil, fmt.Errorf("list legacy firewalld forwarding rules: %w", err)
}
rules, failures := parseLegacyFirewalldForwarding(stdout)
for _, failure := range failures {
if global.LOG != nil {
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
}
}
return rules, nil
}
func parseLegacyFirewalldForwarding(stdout string) ([]legacyFirewalldForward, []legacyFirewalldForwardFailure) {
result := make([]legacyFirewalldForward, 0)
failures := make([]legacyFirewalldForwardFailure, 0)
for _, spec := range strings.Fields(stdout) {
item, err := parseLegacyFirewalldForward(spec)
if err != nil {
failures = append(failures, legacyFirewalldForwardFailure{spec: spec, err: err})
continue
}
result = append(result, item)
}
return result, failures
}
func parseLegacyFirewalldForward(spec string) (legacyFirewalldForward, error) {
if !strings.HasPrefix(spec, "port=") {
return legacyFirewalldForward{}, errors.New("missing port field")
}
port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing protocol field")
}
protocol, rest, ok := strings.Cut(rest, ":toport=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing target port field")
}
targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing target address field")
}
if targetIP == "" {
targetIP = "127.0.0.1"
}
return legacyFirewalldForward{
rule: forwarding.Rule{
Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port,
TargetIP: targetIP, TargetPort: targetPort,
},
spec: spec,
}, nil
}
func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
manager := cmd.NewCommandMgr()
for _, item := range rules {
if err := manager.Run(
"firewall-cmd", "--permanent", "--zone=public", "--remove-forward-port="+item.spec,
); err != nil {
return fmt.Errorf("remove legacy firewalld forwarding rule %q: %w", item.spec, err)
}
}
if len(rules) == 0 {
return nil
}
if err := manager.Run("firewall-cmd", "--reload"); err != nil {
return fmt.Errorf("reload firewalld after forwarding transfer: %w", err)
}
return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart)
}
func restartDockerAfterFirewalldReload(
which func(string) bool,
checkActive func(string) (bool, error),
restart func(string) error,
) error {
const service = "docker"
if !which(service) {
return nil
}
active, err := checkActive(service)
if err != nil {
return fmt.Errorf("check Docker status after reloading firewalld: %w", err)
}
if !active {
return nil
}
if err := restart(service); err != nil {
return fmt.Errorf("restart Docker after reloading firewalld: %w", err)
}
return nil
}
@@ -1,25 +1,21 @@
package utils
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/google/uuid"
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
"gorm.io/gorm"
)
const hostFirewallTransferMigrationID = "host-firewall-transfer"
var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule")
type legacyHostFirewallRecord struct {
@@ -37,126 +33,225 @@ type legacyHostFirewallRecord struct {
Description string
}
func TransferHostFirewall(ctx context.Context, provider string) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
}
return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider))))
type legacyFirewallRuleDescription struct {
UUID string
Provider string
ScopeKey string
Location string
Family string
NativeKind string
Protocol string
SourceAddress string
SourcePort string
DestinationAddress string
DestinationPort string
Interface string
ConnectionStates string
CompatibilityError string
Action string
Priority *int
Description string
Owner string
}
func TransferLegacyHostFirewallRuleOwnership(ctx context.Context, provider string, transfer func(context.Context) error) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
func MigrateHostFirewallDescriptions(db *gorm.DB) error {
providers := []filter.Provider{filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW}
descriptions := make(map[string][]string)
add := func(id, description string) {
if description != "" && !slices.Contains(descriptions[id], description) {
descriptions[id] = append(descriptions[id], description)
}
}
return transferLegacyHostFirewallRuleOwnership(
ctx,
global.DB,
filter.Provider(strings.ToLower(strings.TrimSpace(provider))),
transfer,
)
}
func transferLegacyHostFirewallRuleOwnership(
ctx context.Context,
db *gorm.DB,
provider filter.Provider,
transfer func(context.Context) error,
) error {
if !legacyHostFirewallOwnershipProvider(provider) {
return nil
addRules := func(rules []filter.FirewallRule, description string) bool {
matched := false
for _, rule := range rules {
var err error
switch rule.Scope.Provider {
case filter.ProviderFirewalld:
rule, err = (&filterfirewalld.Adapter{}).PrepareRule(rule)
case filter.ProviderUFW:
rule, err = (&filterufw.Adapter{}).PrepareRule(rule)
}
if err != nil {
continue
}
id, err := filter.DescriptionID(filter.ObservedRule{Rule: rule, ParseStatus: filter.ParseStatusSupported})
if err == nil {
add(id, description)
matched = true
}
}
return matched
}
if db == nil {
return errors.New("host firewall transfer database is required")
}
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
if err != nil || completed {
return err
}
if transfer == nil {
return errors.New("legacy host firewall ownership transfer is required")
}
if err := transfer(ctx); err != nil {
return fmt.Errorf("transfer legacy host firewall rule ownership: %w", err)
}
return markMigrationRecord(db, hostFirewallTransferMigrationID)
}
func legacyHostFirewallOwnershipProvider(provider filter.Provider) bool {
return provider == filter.ProviderIptables || provider == filter.ProviderUFW
}
func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Provider) error {
if db == nil {
return errors.New("host firewall transfer database is required")
}
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
if err != nil || completed {
return err
}
if !isLegacyHostFirewallProvider(provider) {
return fmt.Errorf("unsupported legacy host firewall provider %q", provider)
}
models := make([]model.FirewallRule, 0)
if db.Migrator().HasTable("firewalls") {
var records []legacyHostFirewallRecord
if err := db.WithContext(ctx).Table("firewalls").Order("id ASC").Find(&records).Error; err != nil {
return fmt.Errorf("load legacy host firewall records: %w", err)
}
models = convertLegacyHostFirewallRecords(records, provider)
}
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := importLegacyHostFirewallRules(tx, models); err != nil {
if err := db.Table("firewalls").Order("id ASC").Find(&records).Error; err != nil {
return err
}
if legacyHostFirewallOwnershipProvider(provider) {
return nil
for _, record := range records {
if record.Description == "" {
continue
}
matched := false
for _, provider := range providers {
rules, err := legacyHostFirewallRules(record, provider)
if err == nil && addRules(rules, record.Description) {
matched = true
}
}
if !matched {
add(fmt.Sprintf("firewall:legacy:firewalls:%d", record.ID), record.Description)
}
}
return markMigrationRecord(tx, hostFirewallTransferMigrationID)
})
}
func isLegacyHostFirewallProvider(provider filter.Provider) bool {
switch provider {
case filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW:
return true
default:
return false
}
if db.Migrator().HasTable("firewall_rules") {
var records []legacyFirewallRuleDescription
if err := db.Table("firewall_rules").Order("uuid ASC").Find(&records).Error; err != nil {
return err
}
for _, record := range records {
if record.Description == "" {
continue
}
matched := false
for _, provider := range providers {
rules, err := legacyFirewallDescriptionRules(record, provider)
if err == nil && addRules(rules, record.Description) {
matched = true
}
}
if !matched {
add("firewall:legacy:firewall_rules:"+record.UUID, record.Description)
}
}
}
if len(descriptions) == 0 {
return nil
}
var existing []model.CommonDescription
if err := db.Where("type = ?", "firewall").Find(&existing).Error; err != nil {
return err
}
byID := make(map[string]model.CommonDescription, len(existing))
for _, description := range existing {
byID[description.ID] = description
}
ids := make([]string, 0, len(descriptions))
for id := range descriptions {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
value := strings.Join(descriptions[id], "\n")
if current, ok := byID[id]; ok {
if current.Description == value {
continue
}
if current.Description == "" {
if err := db.Model(&model.CommonDescription{}).Where("id = ?", id).Update("description", value).Error; err != nil {
return err
}
continue
}
id = "firewall:legacy:description:" + strings.TrimPrefix(id, "firewall:")
if _, ok := byID[id]; ok {
continue
}
}
record := model.CommonDescription{ID: id, Type: "firewall", Description: value}
if strings.HasPrefix(id, "firewall:legacy:") {
record.DetailType = "legacy"
}
if err := db.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provider filter.Provider) []model.FirewallRule {
converted := make([]model.FirewallRule, 0, len(records))
byIdentity := make(map[string]int)
for _, record := range records {
rules, err := legacyHostFirewallRules(record, provider)
func legacyFirewallDescriptionRules(record legacyFirewallRuleDescription, provider filter.Provider) ([]filter.FirewallRule, error) {
if record.CompatibilityError != "" {
return nil, errUnsupportedLegacyHostFirewallRule
}
source := filter.Provider(strings.ToLower(strings.TrimSpace(record.Provider)))
scopeParts := strings.Split(record.ScopeKey, ":")
if source == "" && len(scopeParts) > 1 {
source = filter.Provider(scopeParts[0])
}
if source != "" && source != provider {
return nil, nil
}
base := filter.FirewallRule{
Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort,
DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort,
Interface: record.Interface, Action: filter.Action(record.Action),
}
if record.ConnectionStates != "" {
base.ConnectionStates = strings.Split(record.ConnectionStates, ",")
}
if source != "" {
base.NativeKind = filter.NativeKind(record.NativeKind)
}
switch base.NativeKind {
case filter.NativeKindOpaque, filter.NativeKindZoneService, filter.NativeKindUFWApplication:
return nil, errUnsupportedLegacyHostFirewallRule
}
if provider != filter.ProviderUFW && strings.EqualFold(base.Protocol, "all") && base.SourcePort == "" && base.DestinationPort != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = record.Priority
}
families := []filter.Family{filter.Family(record.Family)}
if families[0] == "" {
families[0] = legacyRuleFamily(base.SourceAddress, base.DestinationAddress)
}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
if base.SourceAddress != "" || base.DestinationAddress != "" || base.Protocol == "icmpv6" {
families = []filter.Family{legacyRuleFamily(base.SourceAddress, base.DestinationAddress)}
if base.Protocol == "icmpv6" {
families[0] = filter.FamilyIPv6
}
} else {
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
var result []filter.FirewallRule
for _, family := range families {
rule := base
rule.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
rule.Scope.Table, rule.Scope.Chain = "filter", filter.IptablesInputChain
if source != "" {
if record.Location != "" {
rule.Scope.Chain = record.Location
}
if len(scopeParts) == 5 {
rule.Scope.Table, rule.Scope.Chain = scopeParts[2], scopeParts[3]
}
}
case filter.ProviderFirewalld:
rule.Scope.Zone = filter.FirewalldInputZone
if source != "" && record.Location != "" {
rule.Scope.Zone = record.Location
}
case filter.ProviderUFW:
rule.Scope.Chain = filter.UFWInputChain
}
expanded, err := filter.ExpandAtomicRules(rule)
if err != nil {
if global.LOG != nil {
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err)
}
continue
return nil, err
}
for _, rule := range rules {
item, err := hostFirewallRuleModel(rule)
if err != nil {
if global.LOG != nil {
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err)
}
continue
result = append(result, expanded...)
if source == "" && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
for _, item := range expanded {
item.Scope.Chain = filter.BasicBeforeChain
result = append(result, item)
}
identity := hostFirewallPolicyKey(item)
if index, exists := byIdentity[identity]; exists {
if item.Description != "" {
converted[index].Description = item.Description
}
continue
}
byIdentity[identity] = len(converted)
converted = append(converted, item)
}
}
return converted
return result, nil
}
func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Provider) ([]filter.FirewallRule, error) {
@@ -196,7 +291,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
switch provider {
case filter.ProviderIptables:
rule.Scope = filter.Scope{
Provider: provider, Family: filter.FamilyIPv4, Table: "filter",
Provider: provider, Family: legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress), Table: "filter",
Chain: legacyIptablesChain(record), Direction: filter.DirectionInput,
}
rule.NativeKind = filter.NativeKindRule
@@ -337,84 +432,3 @@ func legacyIPOrPrefix(value string) bool {
_, err := netip.ParsePrefix(value)
return err == nil
}
func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
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.Origin = constant.FirewallRuleOriginAdopted
record.Owner = constant.FirewallRuleSourceUser
record.Revision = 1
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 {
var existing []model.FirewallRule
if err := tx.Find(&existing).Error; err != nil {
return fmt.Errorf("load current host firewall rules: %w", err)
}
byIdentity := make(map[string]model.FirewallRule, len(existing))
for _, item := range existing {
byIdentity[hostFirewallPolicyKey(item)] = item
}
for _, item := range rules {
identity := hostFirewallPolicyKey(item)
if current, exists := byIdentity[identity]; exists {
if current.Description == "" && item.Description != "" {
if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID).
Update("description", item.Description).Error; err != nil {
return fmt.Errorf("restore legacy host firewall description: %w", err)
}
}
continue
}
if err := tx.Create(&item).Error; err != nil {
return fmt.Errorf("import legacy host firewall rule: %w", err)
}
byIdentity[identity] = item
}
return nil
}
+15
View File
@@ -20,6 +20,12 @@ func Init() {
if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil {
panic(err)
}
if err := validator.RegisterValidation("vm_name", checkVMNamePattern); err != nil {
panic(err)
}
if err := validator.RegisterValidation("vm_common", checkVMCommonPattern); err != nil {
panic(err)
}
global.VALID = validator
}
@@ -55,3 +61,12 @@ func checkPasswordPattern(fl validator.FieldLevel) bool {
return false
}
func checkVMNamePattern(fl validator.FieldLevel) bool {
value := fl.Field().String()
return re.GetRegex(re.VMNameValidationPattern).MatchString(value)
}
func checkVMCommonPattern(fl validator.FieldLevel) bool {
value := fl.Field().String()
return re.GetRegex(re.VMCommonPattern).MatchString(value)
}
+3
View File
@@ -27,6 +27,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor)
aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions)
aiToolsRouter.POST("/vllm/monitor/search", baseApi.LoadVLLMMonitor)
aiToolsRouter.POST("/vllm/monitor/current", baseApi.LoadVLLMCurrent)
aiToolsRouter.POST("/vllm/monitor/clean", baseApi.CleanVLLMMonitor)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server/detail", baseApi.LoadMcpServerDetail)
+3 -5
View File
@@ -28,6 +28,8 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
hostRouter.POST("/firewall/family/operate", baseApi.OperateFirewallFamily)
hostRouter.POST("/firewall/settings/ipv6", baseApi.OperateFirewallIPv6)
hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
@@ -36,13 +38,10 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
hostRouter.POST("/firewall/forward/enable", baseApi.EnableForwarding)
hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules)
hostRouter.GET("/firewall/rules/backups", baseApi.ListFirewallRuleBackups)
hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules)
hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail)
hostRouter.POST("/firewall/rules/adopt", baseApi.AdoptFirewallRule)
hostRouter.POST("/firewall/rules", baseApi.CreateFirewallRules)
hostRouter.POST("/firewall/rules/sync/preview", baseApi.PreviewFirewallRuleSync)
hostRouter.GET("/firewall/rules/sync/task", baseApi.LoadFirewallRuleSyncTask)
hostRouter.POST("/firewall/rules/sync", baseApi.SyncFirewallRules)
hostRouter.POST("/firewall/rules/update", baseApi.UpdateFirewallRule)
hostRouter.POST("/firewall/rules/delete", baseApi.DeleteFirewallRules)
hostRouter.POST("/firewall/rules/reorder", baseApi.ReorderFirewallRule)
@@ -50,7 +49,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain)
hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard)
hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts)
hostRouter.POST("/firewall/docker/sync", baseApi.SyncDockerPortGuard)
hostRouter.POST("/firewall/docker/operate", baseApi.OperateDockerPortGuard)
hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies)
hostRouter.POST("/firewall/docker/policies/delete/batch", baseApi.DeleteDockerPortGuardPolicies)
+21 -14
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -22,14 +23,6 @@ type providerResult struct {
}
func New() (bool, Client) {
return newClient()
}
func NewAll() (bool, Client) {
return New()
}
func newClient() (bool, Client) {
client := Client{}
if available, gpuClient := gpu.New(); available {
client.providers = append(client.providers, gpuProvider{client: gpuClient})
@@ -52,6 +45,11 @@ func (c Client) LoadInfo() (*Info, 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))
var wg sync.WaitGroup
for index, item := range c.providers {
@@ -66,8 +64,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
}()
}
wg.Wait()
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot := &Snapshot{DriverVersions: make(map[string]string)}
snapshot := &Snapshot{}
var (
errs []error
active []*ProviderSnapshot
@@ -78,6 +79,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
errs = append(errs, result.err)
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 {
continue
}
@@ -90,11 +96,8 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if item.CudaVersion != "" {
snapshot.Info.CudaVersion = item.CudaVersion
}
if item.DriverVersion != "" {
snapshot.DriverVersions[item.Type] = item.DriverVersion
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
}
snapshot.Warnings = append(snapshot.Warnings, errs...)
@@ -102,6 +105,10 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
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.Type, snapshot.Info.DriverVersion = mergeProviderMetadata(active)
return snapshot, nil
+3 -2
View File
@@ -1,6 +1,7 @@
package accelerator
import (
"math"
"strconv"
"strings"
@@ -18,11 +19,11 @@ func metric(display, unit string) Metric {
return result
}
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
}
normalized, ok := convertMetricUnit(parsed, matched[2], unit)
if !ok {
if !ok || math.IsNaN(normalized) || math.IsInf(normalized, 0) || (unit == "%" && normalized > 100) {
return result
}
result.Value = &normalized
+79 -63
View File
@@ -12,26 +12,38 @@ import (
func normalizeGPU(item *gpu.Device) Device {
metrics := Metrics{
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.MaxPowerLimit, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
MemoryActivity: metric(item.MemoryActivity, "%"),
EncoderUtil: metric(item.EncoderUtil, "%"),
DecoderUtil: metric(item.DecoderUtil, "%"),
JPEGUtil: metric(item.JPEGUtil, "%"),
OFAUtil: metric(item.OFAUtil, "%"),
MediaUtil: metric(item.MediaUtil, "%"),
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{
ID: stableID(item.Type, item.BusID, strconv.FormatUint(uint64(item.Index), 10)),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID(item.Type, item.UUID, stableID("pci", item.BusID, strconv.FormatUint(uint64(item.Index), 10))),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -45,26 +57,36 @@ func normalizeGPU(item *gpu.Device) Device {
func normalizeNPU(item *npu.Device) Device {
metrics := Metrics{
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
AICPUUtil: metric(item.AICPUUtil, "%"),
CtrlCPUUtil: metric(item.CtrlCPUUtil, "%"),
DDRBandwidth: metric(item.DDRBandwidth, "%"),
HBMBandwidth: metric(item.HBMBandwidth, "%"),
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{
ID: fmt.Sprintf("ascend:%d:%d", item.NPUIndex, item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
ProcessStatus: item.ProcessStatus,
ID: fmt.Sprintf("%s:%d", stableID("ascend", item.BusID, strconv.FormatUint(uint64(item.NPUIndex), 10)), item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -78,26 +100,32 @@ func normalizeNPU(item *npu.Device) Device {
func normalizeXPU(item *xpu.Device) Device {
metrics := Metrics{
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"),
MediaUtil: metric(item.Stats.MediaUtil, "%"),
ComputeUtil: metric(item.Stats.ComputeUtil, "%"),
CopyUtil: metric(item.Stats.CopyUtil, "%"),
MediaFrequency: metric(item.Stats.MediaFrequency, "MHz"),
MemoryTemperature: metric(item.Stats.MemoryTemperature, "°C"),
MemoryBandwidth: metric(item.Stats.MemoryBandwidthUtil, "%"),
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{
ID: stableID("xpu", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID)),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID("xpu", item.Basic.UUID, stableID("pci", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID))),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: strconv.Itoa(process.PID),
@@ -110,18 +138,6 @@ func normalizeXPU(item *xpu.Device) 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 {
if busID != "" && !strings.EqualFold(busID, "N/A") {
return vendor + ":" + busID
+17 -1
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"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{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
CudaVersion: info.CudaVersion,
GPUs: info.Devices,
@@ -49,6 +51,7 @@ func (p npuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
NPUs: info.Devices,
}
@@ -71,11 +74,24 @@ func (p xpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
XPUs: info.Devices,
}
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
}
+54 -34
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"errors"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -17,6 +18,9 @@ const (
)
type Info struct {
CollectedAt time.Time `json:"collectedAt"`
Warnings []string `json:"warnings"`
Type string `json:"type"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
@@ -44,25 +48,40 @@ func (m Metric) ValueOrZero() float64 {
}
type Metrics struct {
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
}
MemoryActivity Metric
EncoderUtil Metric
DecoderUtil Metric
JPEGUtil Metric
OFAUtil Metric
MediaUtil Metric
ComputeUtil Metric
CopyUtil 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 {
Utilization bool
Temperature bool
Power bool
PowerLimit bool
Memory bool
FanSpeed bool
Frequency bool
MemoryTemperature Metric
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
MemoryFrequency Metric
}
type Process struct {
@@ -74,18 +93,19 @@ type Process struct {
}
type Device struct {
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Capabilities Capabilities
Processes []Process
ParentID string
ProcessStatus string
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Processes []Process
GPU *gpu.Device `json:"-"`
NPU *npu.Device `json:"-"`
@@ -93,10 +113,9 @@ type Device struct {
}
type Snapshot struct {
Info Info
Devices []Device
DriverVersions map[string]string
Warnings []error
Info Info
Devices []Device
Warnings []error
}
func (s Snapshot) Warning() error {
@@ -104,6 +123,7 @@ func (s Snapshot) Warning() error {
}
type ProviderSnapshot struct {
Warnings []string
Type string
DriverVersion string
CudaVersion string
+58 -17
View File
@@ -3,12 +3,13 @@ package gpu
import (
"context"
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"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) {
var (
staticData string
metricData string
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
staticData string
metricData string
extendedData string
extendedErr error
processData string
staticErr error
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() {
defer wg.Done()
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)
}
if metricErr == nil {
metricErr = applyAMDMetrics(info, metricData)
}
if metricErr != nil {
global.LOG.Warnf("calling %s metric failed, metrics will be omitted: %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)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s metrics: %v", a.command, metricErr))
}
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 {
global.LOG.Warnf("calling %s process failed, process information will be omitted: %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)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s processes: %v", a.command, processErr))
}
return info, nil
}
@@ -96,6 +112,9 @@ func parseAMDStatic(data string) (*Info, error) {
}
device := Device{
Type: "amd",
UUID: amdStringAt(row, "uuid", "asic.uuid"),
DriverVersion: amdStringAt(row, "driver.version", "driver_version", "amdgpu_version"),
ProcessStatus: "unavailable",
Index: index,
ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"),
PersistenceMode: "N/A",
@@ -111,6 +130,7 @@ func parseAMDStatic(data string) (*Info, error) {
"limit.max_power_limit",
"limit.max_power",
),
PowerLimit: amdMetricAt(row, "W", "limit.ppt0.socket_power_limit", "limit.socket_power_limit"),
MemUsed: "N/A",
MemTotal: "N/A",
GPUUtil: "N/A",
@@ -150,11 +170,31 @@ func applyAMDMetrics(info *Info, data string) error {
continue
}
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.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.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 != "" {
device.PerformanceState = value
}
@@ -177,6 +217,7 @@ func applyAMDProcesses(info *Info, data string) error {
if !ok {
continue
}
device.ProcessStatus = "ok"
processList, _ := amdValueAt(row, "process_list")
items := amdObjectList(processList)
if len(items) == 0 {
+4
View File
@@ -64,6 +64,7 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if result.info == nil {
continue
}
merged.Warnings = append(merged.Warnings, result.info.Warnings...)
if 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 {
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
}
+76 -69
View File
@@ -1,16 +1,12 @@
package gpu
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -24,41 +20,10 @@ func (n nvidiaSMI) LoadInfo(ctx context.Context) (*Info, error) {
if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", nvidiaSMICommand, err)
}
data := []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)
return parseNvidiaSMI([]byte(itemData))
}
func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
func parseNvidiaSMI(buf []byte) (*Info, error) {
var (
s nvidiaSMIResponse
info Info
@@ -72,42 +37,84 @@ func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
info.DriverVersion = s.DriverVersion
for i := range s.Gpu {
gpuItem := Device{
Type: "nvidia",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccErrors.Volatile.DramUncorrectable,
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,
}
if version == "v12" || version == "v13" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
if gpuItem.PowerDraw == "" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.InstantPowerDraw
}
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.CurrentPowerLimit
} else {
gpuItem.PowerDraw = s.Gpu[i].PowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].PowerReadings.MaxPowerLimit
Type: "nvidia",
MemoryActivity: s.Gpu[i].Utilization.MemoryUtil,
EncoderUtil: s.Gpu[i].Utilization.EncoderUtil,
DecoderUtil: s.Gpu[i].Utilization.DecoderUtil,
JPEGUtil: s.Gpu[i].Utilization.JpegUtil,
OFAUtil: s.Gpu[i].Utilization.OfaUtil,
MediaFrequency: s.Gpu[i].Clocks.VideoClock,
UUID: s.Gpu[i].UUID,
DriverVersion: s.DriverVersion,
Architecture: s.Gpu[i].ProductArchitecture,
Frequency: s.Gpu[i].Clocks.GraphicsClock,
MemoryFrequency: s.Gpu[i].Clocks.MemClock,
MemoryTemperature: s.Gpu[i].Temperature.MemoryTemp,
MemoryFree: s.Gpu[i].FbMemoryUsage.Free,
MemoryReserved: s.Gpu[i].FbMemoryUsage.Reserved,
PCIeGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.DeviceCurrentLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.CurrentLinkGen),
PCIeMaxGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxDeviceLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxLinkGen),
PCIeWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.CurrentLinkWidth,
PCIeMaxWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.MaxLinkWidth,
ProcessStatus: "unavailable",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
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 {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
{Scope: "Volatile DRAM", Correctable: s.Gpu[i].EccErrors.Volatile.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DramUncorrectable},
{Scope: "Volatile SRAM", Correctable: s.Gpu[i].EccErrors.Volatile.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.SramUncorrectable},
{Scope: "Aggregate DRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DramUncorrectable},
{Scope: "Aggregate SRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.SramUncorrectable},
}
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)
}
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
import "encoding/xml"
type nvidiaSMIResponse struct {
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
XMLName xml.Name `xml:"nvidia_smi_log"`
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
Gpu []struct {
ID string `xml:"id,attr"`
AccountedProcesses struct{} `xml:"accounted_processes"`
@@ -37,16 +40,17 @@ type nvidiaSMIResponse struct {
VideoClock string `xml:"video_clock"`
} `xml:"clocks"`
ClocksEventReasons struct {
ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"`
ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"`
ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"`
ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"`
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"`
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `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"`
DefaultApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"`
@@ -63,12 +67,24 @@ type nvidiaSMIResponse struct {
} `xml:"driver_model"`
EccErrors 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"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"aggregate"`
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"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
@@ -226,7 +242,8 @@ type nvidiaSMIResponse struct {
MinPowerLimit string `xml:"min_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
} `xml:"power_readings"`
Processes struct {
Processes *struct {
Text string `xml:",chardata"`
ProcessInfo []struct {
Pid string `xml:"pid"`
Type string `xml:"type"`
+37
View File
@@ -1,6 +1,8 @@
package gpu
type Info struct {
Warnings []string `json:"warnings"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
Type string `json:"type"`
@@ -8,7 +10,42 @@ type Info struct {
Devices []Device `json:"gpu"`
}
type ECCError struct {
Scope string `json:"scope"`
Correctable string `json:"correctable"`
Uncorrectable string `json:"uncorrectable"`
}
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"`
Index uint `json:"index"`
ProductName string `json:"productName"`
+61 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -34,7 +35,31 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if err != nil {
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 {
@@ -158,6 +183,12 @@ func parseAscendSMI(data string) *Info {
pending = nil
}
for i := range info.Devices {
info.Devices[i].ProcessStatus = "unavailable"
if processSection {
info.Devices[i].ProcessStatus = "ok"
}
}
return info
}
@@ -208,6 +239,9 @@ func ascendMemoryPools(value, header string) (string, string, string, string) {
}
hbm := usage[len(usage)-1]
if !strings.Contains(normalizedHeader, "MEMORYUSAGE") {
memoryUsed, memoryTotal = "", ""
}
return memoryUsed, memoryTotal, hbm[0] + " MB", hbm[1] + " MB"
}
@@ -233,3 +267,29 @@ func ascendValueWithUnit(value, unit string) string {
}
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
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
Devices []Device `json:"npu"`
}
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"`
Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
+335
View File
@@ -0,0 +1,335 @@
package vllm
import (
"bufio"
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type Metrics map[string]map[string]float64
type histogram struct {
Sum float64 `json:"sum"`
Count float64 `json:"count"`
Buckets map[string]float64 `json:"buckets,omitempty"`
}
func Parse(reader io.Reader) (Metrics, error) {
metrics := Metrics{}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "vllm:") {
continue
}
match := re.GetRegex(re.VLLMMetricSamplePattern).FindStringSubmatch(line)
if match == nil {
return nil, fmt.Errorf("invalid vLLM metric sample")
}
name := strings.TrimPrefix(match[1], "vllm:")
switch name {
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total":
default:
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
if base == name {
continue
}
switch base {
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds":
default:
continue
}
}
value, err := strconv.ParseFloat(match[3], 64)
if err != nil {
return nil, fmt.Errorf("invalid vLLM metric value: %w", err)
}
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
continue
}
if metrics[name] == nil {
metrics[name] = map[string]float64{}
}
labels := re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(match[2], -1)
parts := make([]string, 0, len(labels))
for _, label := range labels {
parts = append(parts, label[1]+"="+label[2])
}
sort.Strings(parts)
metrics[name]["{"+strings.Join(parts, ",")+"}"] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(metrics) == 0 {
return nil, fmt.Errorf("no supported vLLM metrics")
}
return metrics, nil
}
func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, error) {
result := model.MonitorVLLM{}
for name, target := range map[string]**float64{
"num_requests_running": &result.Running,
"num_requests_waiting": &result.Waiting,
} {
if len(current[name]) == 0 {
continue
}
value := 0.0
for _, sample := range current[name] {
value += sample
}
*target = &value
}
cache := current["kv_cache_usage_perc"]
if len(cache) == 0 {
cache = current["gpu_cache_usage_perc"]
}
if len(cache) > 0 {
value := 0.0
for _, sample := range cache {
value += sample
}
value = value / float64(len(cache)) * 100
result.CacheUsage = &value
}
if elapsed <= 0 {
return result, nil
}
for name, target := range map[string]**float64{
"prompt_tokens_total": &result.PromptThroughput,
"generation_tokens_total": &result.GenerationThroughput,
"request_success_total": &result.RequestThroughput,
} {
if value := counterDelta(current[name], previous[name]); value != nil {
rate := *value / elapsed
*target = &rate
}
}
histograms := map[string]histogram{}
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} {
source := name
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
source = "time_per_output_token_seconds"
}
sum := counterDelta(current[source+"_sum"], previous[source+"_sum"])
count := counterDelta(current[source+"_count"], previous[source+"_count"])
if sum == nil || count == nil {
continue
}
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)}
}
applyHistograms(&result, histograms)
encoded, err := json.Marshal(histograms)
if err != nil {
return model.MonitorVLLM{}, err
}
result.HistogramDeltas = string(encoded)
return result, nil
}
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error {
var samples []map[string]histogram
if err := json.Unmarshal([]byte(encoded), &samples); err != nil {
return err
}
merged := map[string]histogram{}
for _, sample := range samples {
for name, item := range sample {
if item.Count == 0 {
continue
}
total, exists := merged[name]
if !exists {
merged[name] = item
continue
}
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
merged[name] = total
}
}
applyHistograms(result, merged)
return nil
}
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
for _, item := range []struct {
name string
mean **float64
quantiles []**float64
}{
{"time_to_first_token_seconds", &result.TimeToFirstToken, []**float64{&result.TimeToFirstTokenP50, &result.TimeToFirstTokenP90, &result.TimeToFirstTokenP95, &result.TimeToFirstTokenP99}},
{"inter_token_latency_seconds", &result.TimePerOutputToken, []**float64{&result.TimePerOutputTokenP50, &result.TimePerOutputTokenP90, &result.TimePerOutputTokenP95, &result.TimePerOutputTokenP99}},
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
{"request_prefill_time_seconds", &result.PrefillTime, nil},
{"request_decode_time_seconds", &result.DecodeTime, nil},
} {
histogram, ok := histograms[item.name]
if !ok || histogram.Count <= 0 {
continue
}
mean := histogram.Sum / histogram.Count
*item.mean = &mean
if len(item.quantiles) == 0 {
continue
}
quantiles := histogramQuantiles(histogram.Buckets)
for i, target := range item.quantiles {
*target = quantiles[i]
}
}
}
func counterDelta(current, previous map[string]float64) *float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
value := 0.0
for label, sample := range current {
before, ok := previous[label]
if !ok || sample < before {
return nil
}
value += sample - before
}
if math.IsInf(value, 0) || math.IsNaN(value) {
return nil
}
return &value
}
func bucketDeltas(current, previous map[string]float64, count float64) map[string]float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
groups := map[string]map[string]float64{}
for labels, sample := range current {
before, ok := previous[labels]
if !ok || sample < before {
return nil
}
bound := ""
var identity []string
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
if label[1] == "le" {
raw, err := strconv.Unquote(label[2])
if err != nil {
return nil
}
upper, err := strconv.ParseFloat(raw, 64)
if err != nil || math.IsNaN(upper) || upper < 0 {
return nil
}
bound = strconv.FormatFloat(upper, 'g', -1, 64)
} else {
identity = append(identity, label[1]+"="+label[2])
}
}
if bound == "" {
return nil
}
group := strings.Join(identity, ",")
if groups[group] == nil {
groups[group] = map[string]float64{}
}
if _, exists := groups[group][bound]; exists {
return nil
}
groups[group][bound] = sample - before
}
result := map[string]float64{}
for _, buckets := range groups {
if _, ok := buckets["+Inf"]; !ok || len(buckets) < 2 {
return nil
}
if len(result) > 0 && len(result) != len(buckets) {
return nil
}
for bound := range result {
if _, ok := buckets[bound]; !ok {
return nil
}
}
bounds := make([]float64, 0, len(buckets))
for bound := range buckets {
value, _ := strconv.ParseFloat(bound, 64)
bounds = append(bounds, value)
}
sort.Float64s(bounds)
previousCount := 0.0
for _, bound := range bounds {
key := strconv.FormatFloat(bound, 'g', -1, 64)
value := buckets[key]
if value < previousCount {
if previousCount-value > 1e-12*(previousCount+value) {
return nil
}
value = previousCount
}
result[key] += value
previousCount = value
}
}
if math.Abs(result["+Inf"]-count) > 1e-12*(result["+Inf"]+count) {
return nil
}
return result
}
func histogramQuantiles(buckets map[string]float64) [4]*float64 {
var quantiles [4]*float64
if len(buckets) < 2 || buckets["+Inf"] <= 0 {
return quantiles
}
bounds := make([]float64, 0, len(buckets)-1)
for bound := range buckets {
value, err := strconv.ParseFloat(bound, 64)
if err != nil || math.IsNaN(value) || value < 0 {
return quantiles
}
if !math.IsInf(value, 1) {
bounds = append(bounds, value)
}
}
if len(bounds) == 0 {
return quantiles
}
sort.Float64s(bounds)
for i, q := range [...]float64{0.5, 0.9, 0.95, 0.99} {
rank := q * buckets["+Inf"]
lower, before := 0.0, 0.0
value := bounds[len(bounds)-1]
for _, upper := range bounds {
count := buckets[strconv.FormatFloat(upper, 'g', -1, 64)]
if count >= rank && count > before {
value = lower + (upper-lower)*(rank-before)/(count-before)
break
}
lower, before = upper, count
}
quantiles[i] = &value
}
return quantiles
}
+28 -2
View File
@@ -33,16 +33,29 @@ type discoveryInfo struct {
}
type DeviceLevelMetric struct {
MetricsType string `json:"metrics_type"`
Value float64 `json:"value"`
MetricsType string `json:"metrics_type"`
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 {
TileLevel []TileMetrics `json:"tile_level"`
DeviceID int `json:"device_id"`
DeviceLevel []DeviceLevelMetric `json:"device_level"`
}
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
@@ -50,12 +63,17 @@ type Info struct {
}
type Device struct {
Tiles []TileStats `json:"tiles"`
ProcessStatus string `json:"processStatus"`
Basic Basic `json:"basic"`
Stats Stats `json:"stats"`
Processes []Process `json:"processes"`
}
type Basic struct {
UUID string `json:"uuid"`
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
VendorName string `json:"vendorName"`
@@ -66,6 +84,14 @@ type Basic 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"`
GPUUtil string `json:"gpuUtil"`
Frequency string `json:"frequency"`
+53 -13
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"sync"
@@ -51,12 +52,15 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
processData, err := cmdMgr.RunWithStdout(xpuSMICommand, "ps", "-j")
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 {
var psList DeviceUtilByProcList
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 {
for i := range res.Devices {
res.Devices[i].ProcessStatus = "ok"
}
for _, ps := range psList.DeviceUtilByProcList {
process := Process{
PID: ps.ProcessID,
@@ -87,7 +91,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
defer wg.Done()
xpu := Device{
ProcessStatus: "unavailable",
Basic: Basic{
UUID: device.UUID,
DeviceID: device.DeviceID,
DeviceName: device.DeviceName,
VendorName: device.VendorName,
@@ -115,13 +121,19 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
wgCmd.Wait()
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
}
var info discoveryDevice
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
}
@@ -142,13 +154,22 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
}
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 {
var stats DeviceStats
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 {
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) {
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 {
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":
stats.Power = fmt.Sprintf("%.1fW", stat.Value)
stats.Power = fmt.Sprintf("%.1fW", value)
case "XPUM_STATS_GPU_UTILIZATION":
stats.GPUUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.GPUUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_GPU_FREQUENCY":
stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value)
stats.Frequency = fmt.Sprintf("%.1fMHz", value)
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":
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", stat.Value)
case "XPUM_STATS_MEMORY_UTILIZATION", "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", value)
case "XPUM_STATS_MEMORY_UTILIZATION":
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"
)
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 {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule
@@ -76,14 +76,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient,
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
}
msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("PanelAlertTitle"),
Body: content,
IsHTML: true,
IsHTML: GetCronJobType(alert.Type) != "cronJob",
}
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)
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
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{
Type: GetCronJobType(alert.Type),
SubType: alert.Type,
Title: alert.Title,
Title: TaskAlertTitle(alert.Type, alert.Title, project, params),
Method: method,
Project: project,
Params: params,
@@ -281,6 +281,14 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
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 {
marshal, err := json.Marshal(alert)
if err != nil {
@@ -324,6 +332,10 @@ func GetCronJobTypeName(cronJobType string) string {
module = "系统快照"
case "ntp":
module = "同步服务器时间"
case "syncIpGroup":
module = "同步 IP 组"
case "cleanLog":
module = "清理日志"
default:
}
return module
@@ -444,6 +456,30 @@ func isWithinTimeRange(savedTimeString string) bool {
}
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) {
case "ssl":
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":
return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
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":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
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,
agentInfo *dto.AgentInfo,
) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertInfo := info
alertInfo.Type = alertType
create.Type = GetCronJobType(alertType)
@@ -99,7 +99,7 @@ func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occur
if businessType == "" {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
}
content := GetSendContent(businessType, detail.Params, agentInfo)
content := GetAlertDetailContent(detail, agentInfo)
if content == "" {
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
_ = copier.Copy(&alert, &alertInfo)
if alertUtil.GetCronJobType(pushAlert.AlertType) == "cronJob" && !alertUtil.MatchCronJobAlertResult(alert.AdvancedParams, pushAlert.Result) {
return nil
}
methods := strings.Split(alert.Method, ",")
for _, m := range methods {
@@ -126,7 +129,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
params := alertUtil.CreateTaskAlertParams(pushAlert)
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule
+10 -2
View File
@@ -97,7 +97,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}()
defer close(done)
client, err := sftp.NewClient(sshClient)
client, err := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true))
if err != nil {
return false, err
}
@@ -127,7 +127,15 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
// ReadFrom uses the pipelined write path of pkg/sftp. A plain io.Copy would
// pick os.File.WriteTo (the source is an *os.File), which issues one
// synchronous SFTP WRITE per packet and waits for the reply every time, so
// the throughput is bounded by packetSize/RTT.
written, err := dstFile.ReadFrom(srcFile)
if err != nil {
// A failed concurrent write may leave holes in the remote file, so cut
// it back to the number of bytes actually written.
_ = dstFile.Truncate(written)
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
+5 -4
View File
@@ -1,6 +1,7 @@
package docker_guard
import (
"context"
"net/netip"
"path/filepath"
"slices"
@@ -13,8 +14,8 @@ import (
"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"))
func ReadDNATRules(ctx context.Context, backend, family string) DNATRules {
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
if backend == constant.FirewallProviderNftables {
tableFamily := "ip"
if family == constant.FirewallFamilyIPv6 {
@@ -45,8 +46,8 @@ func ReadDNATRules(backend, family string) DNATRules {
return DNATRules{Output: output, Inspected: err == nil}
}
func ReadProxyEndpoints() ProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
func ReadProxyEndpoints(ctx context.Context) ProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil {
return ProxyEndpoints{}
+90 -64
View File
@@ -1,10 +1,11 @@
package docker_guard
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort"
"slices"
"strconv"
"strings"
"sync"
@@ -12,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "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/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
@@ -22,26 +24,27 @@ type Runner interface {
Exists(executable string) bool
}
type commandRunner struct{}
type commandRunner struct{ ctx context.Context }
func (commandRunner) Run(executable string, args ...string) (string, error) {
func (r commandRunner) Run(executable string, args ...string) (string, error) {
executable = dockerGuardExecutable(executable)
manager := cmd.NewCommandMgr(cmd.WithTimeout(60 * time.Second))
manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second))
stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...)
err = errors.Join(err, r.ctx.Err())
if err != nil {
return stdout, fmt.Errorf("command=%s %s failed: %w", executable, strings.Join(args, " "), err)
}
return stdout, nil
}
func (commandRunner) RunInput(executable, input string, args ...string) (string, error) {
func (r commandRunner) RunInput(executable, input string, args ...string) (string, error) {
executable = dockerGuardExecutable(executable)
if executable == "nft" {
return "", nftables_helper.RunScript(input)
return "", errors.Join(nftables_helper.RunScriptContext(r.ctx, input), r.ctx.Err())
}
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)))
manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)))
stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...)
return stdout, firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err)
return stdout, errors.Join(firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err), r.ctx.Err())
}
func (commandRunner) Exists(executable string) bool {
@@ -74,9 +77,12 @@ type Iptables struct {
var mutationMu sync.Mutex
func NewIptables() *Iptables { return &Iptables{runner: commandRunner{}} }
func NewIptables(ctx context.Context) *Iptables { return &Iptables{runner: commandRunner{ctx: ctx}} }
func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) error {
func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if err := CheckIPv4Forwarding(); err != nil {
@@ -88,7 +94,7 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro
if err := m.ensureFamily("iptables", true); err != nil {
return err
}
if m.runner.Exists("ip6tables") {
if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
available, err := m.chainExists("ip6tables", DockerChain)
if err != nil {
return &FamilyError{Family: FamilyIPv6, Err: fmt.Errorf("inspect %s chain: %w", DockerChain, err)}
@@ -102,16 +108,19 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro
}
}
}
return m.rebuildLocked(policies, inventory)
return m.rebuildLocked(policies, inventory, families...)
}
func (m *Iptables) Bind() error {
func (m *Iptables) Bind(families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if err := m.bindExistingFamily("iptables", true); err != nil {
return err
}
if m.runner.Exists("ip6tables") {
if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
if err := m.bindExistingFamily("ip6tables", false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
}
@@ -126,13 +135,16 @@ func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory)
}
func (m *Iptables) ListPolicies() (PolicyInventory, error) {
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) {
continue
}
exists, err := m.chainExists(executable, Chain)
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
}
@@ -148,22 +160,21 @@ func (m *Iptables) ListPolicies() (PolicyInventory, error) {
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
}
inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
for key, orders := range parsed.ManagedRuleOrders {
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
for key, orders := range parsed.RuleOrders {
inventory.RuleOrders[key] = append([]int64(nil), orders...)
}
}
return inventory, nil
}
func (m *Iptables) Unbind() error {
func (m *Iptables) Unbind(families ...string) error {
mutationMu.Lock()
defer mutationMu.Unlock()
if err := m.unbindFamily("iptables"); err != nil {
return err
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
if m.runner.Exists("ip6tables") {
if err := m.unbindFamily("ip6tables"); err != nil {
for _, family := range families {
if err := m.unbindFamily(executableForFamily(family)); err != nil {
return err
}
}
@@ -315,13 +326,19 @@ func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
return nil
}
func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
for _, family := range families {
executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) {
continue
}
exists, err := m.chainExists(executable, Chain)
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) && !slices.ContainsFunc(policies, func(policy Policy) bool { return policy.Family == family }) {
continue
}
if err != nil {
return &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
}
@@ -346,41 +363,34 @@ func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) e
return nil
}
type orderedIPTablesRule struct {
order int64
index int
rules [][]string
}
func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
segments := make([]orderedIPTablesRule, 0, len(policies)+len(inventory.ReadOnly))
segments := make([]orderedPolicyRule, 0, len(policies))
maxOrder := int64(0)
index := 0
for _, orders := range inventory.ManagedRuleOrders {
for _, orders := range inventory.RuleOrders {
for _, order := range orders {
if order > maxOrder {
maxOrder = order
}
maxOrder = max(maxOrder, order)
}
}
for _, item := range inventory.ReadOnly {
for _, native := range item.NativeRules {
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain {
continue
}
segments = append(segments, orderedIPTablesRule{order: native.Order, index: index, rules: [][]string{append([]string(nil), native.Tokens...)}})
index++
if native.Order > maxOrder {
maxOrder = native.Order
}
for _, policy := range policies {
for _, rule := range policy.NativeRules {
maxOrder = max(maxOrder, rule.Order)
}
}
for _, policy := range policies {
if policy.Family != family {
continue
}
if len(policy.NativeRules) > 0 {
for _, native := range policy.NativeRules {
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain {
continue
}
segments = append(segments, orderedPolicyRule{order: native.Order, rule: append([]string(nil), native.Tokens...)})
}
continue
}
compiled := compilePolicy(policy)
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled {
order := int64(0)
if ruleIndex < len(orders) {
@@ -389,21 +399,10 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
maxOrder++
order = maxOrder
}
segments = append(segments, orderedIPTablesRule{order: order, index: index, rules: [][]string{rule}})
index++
segments = append(segments, orderedPolicyRule{order: order, rule: rule})
}
}
sort.SliceStable(segments, func(left, right int) bool {
if segments[left].order == segments[right].order {
return segments[left].index < segments[right].index
}
return segments[left].order < segments[right].order
})
rules := make([][]string, 0)
for _, segment := range segments {
rules = append(rules, segment.rules...)
}
return rules
return sortPolicyRules(segments)
}
func buildRestoreScript(rules [][]string) (string, error) {
@@ -436,14 +435,20 @@ func compilePolicy(policy Policy) [][]string {
}
base = append(base, "--ctorigdstport", strconv.Itoa(int(policy.HostPort)))
comment := "1panel-docker:" + policy.UUID
if policy.Mode == ModeAll {
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", "DROP")}
if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll {
target := "DROP"
if policy.Mode == ModeAcceptAll {
target = "ACCEPT"
}
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", target)}
}
target := "DROP"
capacity := len(policy.Sources)
if policy.Mode == ModeAllow {
target = "RETURN"
capacity++
} else if policy.Mode == ModeAcceptSources {
target = "ACCEPT"
}
rules := make([][]string, 0, capacity)
for _, source := range policy.Sources {
@@ -491,7 +496,11 @@ func chainDeclared(output, chain string) bool {
func (m *Iptables) run(executable string, args ...string) (string, error) {
commandArgs := append([]string{"-w", "-t", "filter"}, args...)
return m.runner.Run(executable, commandArgs...)
output, err := m.runner.Run(executable, commandArgs...)
if executable == "ip6tables" && err != nil && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}
func executableForFamily(family string) string {
@@ -533,3 +542,20 @@ func isWildcardHost(family, hostIP string) bool {
return (family == FamilyIPv4 && (hostIP == "" || hostIP == "0.0.0.0")) ||
(family == FamilyIPv6 && (hostIP == "" || hostIP == "::"))
}
func (m *Iptables) OperateFamily(family string, initialize bool) error {
if family != FamilyIPv4 && family != FamilyIPv6 {
return fmt.Errorf("unsupported Docker firewall family %q", family)
}
if family == FamilyIPv4 {
if err := CheckIPv4Forwarding(); err != nil {
return err
}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if initialize {
return m.ensureFamily(executableForFamily(family), true)
}
return m.bindExistingFamily(executableForFamily(family), true)
}
+100 -71
View File
@@ -1,13 +1,15 @@
package docker_guard
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort"
"slices"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
@@ -23,9 +25,12 @@ type Nftables struct {
runner Runner
}
func NewNftables() *Nftables { return &Nftables{runner: commandRunner{}} }
func NewNftables(ctx context.Context) *Nftables { return &Nftables{runner: commandRunner{ctx: ctx}} }
func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) error {
func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if !m.runner.Exists("nft") {
@@ -34,26 +39,33 @@ func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) erro
if err := CheckIPv4Forwarding(); err != nil {
return err
}
if err := m.checkForwardPolicy(); err != nil {
if err := m.checkForwardPolicy(families...); err != nil {
return err
}
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
return err
}
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
if slices.Contains(families, FamilyIPv6) {
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
}
}
return m.rebuildLocked(policies, inventory)
return m.rebuildLocked(policies, inventory, families...)
}
func (m *Nftables) Bind() error {
func (m *Nftables) Bind(families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
return err
}
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
if slices.Contains(families, FamilyIPv6) {
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
}
}
return nil
}
@@ -68,11 +80,11 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
if !m.runner.Exists("nft") {
return PolicyInventory{}, nil
}
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain)
if errors.Is(err, nftables_helper.ErrChainNotFound) {
if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) {
continue
}
if err != nil {
@@ -83,18 +95,20 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
}
inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
for key, orders := range parsed.ManagedRuleOrders {
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
for key, orders := range parsed.RuleOrders {
inventory.RuleOrders[key] = append([]int64(nil), orders...)
}
}
return inventory, nil
}
func (m *Nftables) Unbind() error {
func (m *Nftables) Unbind(families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
mutationMu.Lock()
defer mutationMu.Unlock()
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
for _, family := range families {
if err := m.unbindFamily(family); err != nil {
return &FamilyError{Family: family, Err: err}
}
@@ -106,12 +120,16 @@ func (m *Nftables) Cleanup() error {
mutationMu.Lock()
defer mutationMu.Unlock()
if !m.runner.Exists("nft") {
return nil
return errors.New("nft is not installed")
}
commands := make([][]string, 0, 2)
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
if !m.objectExists("table", tableFamily, NftTable) {
_, exists, err := nftables_helper.ReadTable(m.run, tableFamily, NftTable)
if err != nil {
return err
}
if !exists {
continue
}
commands = append(commands, []string{"delete", "table", tableFamily, NftTable})
@@ -160,7 +178,7 @@ func (m *Nftables) Status(family string) FamilyStatus {
}
}
if !baseExists || !guardExists {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing, Partial: baseExists || guardExists}
}
status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules := baseRules.String()
@@ -260,11 +278,14 @@ func (m *Nftables) ensureJump(family string) error {
return m.runBatch(commands)
}
func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error {
if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
if !m.runner.Exists("nft") {
return nil
}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
for _, family := range families {
tableFamily := nftTableFamily(family)
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
continue
@@ -284,44 +305,37 @@ func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) e
return nil
}
type orderedNftRule struct {
order int64
index int
rules [][]string
}
func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
tableFamily := nftTableFamily(family)
segments := make([]orderedNftRule, 0, len(policies)+len(inventory.ReadOnly))
segments := make([]orderedPolicyRule, 0, len(policies))
maxOrder := int64(0)
index := 0
for _, orders := range inventory.ManagedRuleOrders {
for _, orders := range inventory.RuleOrders {
for _, order := range orders {
if order > maxOrder {
maxOrder = order
}
maxOrder = max(maxOrder, order)
}
}
for _, item := range inventory.ReadOnly {
for _, native := range item.NativeRules {
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" {
continue
}
command := []string{"add", "rule", tableFamily, NftTable, NftChain}
command = append(command, quoteNftTokens(native.Tokens)...)
segments = append(segments, orderedNftRule{order: native.Order, index: index, rules: [][]string{command}})
index++
if native.Order > maxOrder {
maxOrder = native.Order
}
for _, policy := range policies {
for _, rule := range policy.NativeRules {
maxOrder = max(maxOrder, rule.Order)
}
}
for _, policy := range policies {
if policy.Family != family {
continue
}
if len(policy.NativeRules) > 0 {
for _, native := range policy.NativeRules {
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" {
continue
}
command := []string{"add", "rule", tableFamily, NftTable, NftChain}
command = append(command, quoteNftTokens(native.Tokens)...)
segments = append(segments, orderedPolicyRule{order: native.Order, rule: command})
}
continue
}
compiled := compileNftPolicy(policy)
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled {
order := int64(0)
if ruleIndex < len(orders) {
@@ -330,27 +344,16 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
maxOrder++
order = maxOrder
}
segments = append(segments, orderedNftRule{order: order, index: index, rules: [][]string{rule}})
index++
segments = append(segments, orderedPolicyRule{order: order, rule: rule})
}
}
sort.SliceStable(segments, func(left, right int) bool {
if segments[left].order == segments[right].order {
return segments[left].index < segments[right].index
}
return segments[left].order < segments[right].order
})
rules := make([][]string, 0)
for _, segment := range segments {
rules = append(rules, segment.rules...)
}
return rules
return sortPolicyRules(segments)
}
func quoteNftTokens(tokens []string) []string {
quoted := make([]string, 0, len(tokens))
for _, token := range tokens {
if strings.ContainsAny(token, " \t\\\"'") && !strings.HasPrefix(token, `"`) {
for index, token := range tokens {
if (index > 0 && tokens[index-1] == "comment" || strings.ContainsAny(token, " \t\\\"'")) && !strings.HasPrefix(token, `"`) {
quoted = append(quoted, strconv.Quote(token))
continue
}
@@ -367,15 +370,22 @@ func compileNftPolicy(policy Policy) [][]string {
base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP)
}
base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort)))
comment := strconv.Quote("1panel-docker:" + policy.UUID)
if policy.Mode == ModeAll {
return [][]string{append(append([]string{}, base...), "drop", "comment", comment)}
marker := "1panel-docker:" + policy.UUID
comment := strconv.Quote(marker)
if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll {
target := "drop"
if policy.Mode == ModeAcceptAll {
target = "accept"
}
return [][]string{append(append([]string{}, base...), target, "comment", comment)}
}
target := "drop"
capacity := len(policy.Sources)
if policy.Mode == ModeAllow {
target = "return"
capacity++
} else if policy.Mode == ModeAcceptSources {
target = "accept"
}
rules := make([][]string, 0, capacity)
for _, source := range policy.Sources {
@@ -506,11 +516,14 @@ func nftHasFirstUniqueJump(output string) bool {
return false
}
func (m *Nftables) checkForwardPolicy() error {
func (m *Nftables) checkForwardPolicy(families ...string) error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if len(families) > 0 && !slices.Contains(families, family.name) {
continue
}
if !m.runner.Exists(family.command) {
continue
}
@@ -526,11 +539,7 @@ func (m *Nftables) checkForwardPolicy() error {
}
found = true
if fields[2] == "DROP" {
label := "IPv4"
if family.name == FamilyIPv6 {
label = "IPv6"
}
return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)}
return &FamilyError{Family: family.name, Err: buserr.New("ErrDockerForwardPolicyDrop")}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
@@ -542,3 +551,23 @@ func (m *Nftables) checkForwardPolicy() error {
}
return nil
}
func (m *Nftables) OperateFamily(family string, initialize bool) error {
if family != FamilyIPv4 && family != FamilyIPv6 {
return fmt.Errorf("unsupported Docker firewall family %q", family)
}
if family == FamilyIPv4 {
if err := CheckIPv4Forwarding(); err != nil {
return err
}
}
mutationMu.Lock()
defer mutationMu.Unlock()
if err := m.checkForwardPolicy(family); err != nil {
return err
}
if initialize {
return m.ensureFamily(family, true)
}
return m.bindExistingFamily(family, true)
}
+144 -39
View File
@@ -1,8 +1,11 @@
package docker_guard
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@@ -12,9 +15,7 @@ import (
type observedPolicy struct {
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
orders []int64
dropAll bool
droppedSource []string
allowedSource []string
@@ -22,6 +23,106 @@ type observedPolicy struct {
acceptAll bool
}
func ConvertPolicyBackend(policy Policy, sourceBackend, targetBackend string) (Policy, error) {
compiled := make(map[string][][]string, 2)
for _, backend := range []string{sourceBackend, targetBackend} {
switch backend {
case "iptables":
compiled[backend] = compilePolicy(policy)
case "nftables":
compiled[backend] = compileNftPolicy(policy)
for i := range compiled[backend] {
compiled[backend][i] = compiled[backend][i][5:]
}
default:
return Policy{}, fmt.Errorf("unsupported Docker firewall backend %q", backend)
}
}
if len(policy.NativeRules) == 0 {
return policy, nil
}
sourceRules, targetRules := compiled[sourceBackend], compiled[targetBackend]
if len(policy.NativeRules) != len(sourceRules) {
return Policy{}, fmt.Errorf("Docker policy %s contains native rules that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
}
byRule := make(map[string]int, len(sourceRules))
for index, rule := range sourceRules {
byRule[dockerPolicyRuleKey(rule, sourceBackend)] = index
}
converted := make([]NativeRule, 0, len(policy.NativeRules))
for _, native := range policy.NativeRules {
key := dockerPolicyRuleKey(native.Tokens, sourceBackend)
index, exists := byRule[key]
if !exists || native.Family != policy.Family {
return Policy{}, fmt.Errorf("Docker policy %s contains native conditions that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
}
delete(byRule, key)
tokens := append([]string(nil), targetRules[index]...)
for i, token := range tokens {
if unquoted, err := strconv.Unquote(token); err == nil {
tokens[i] = unquoted
}
}
converted = append(converted, NativeRule{Family: native.Family, Order: native.Order, Tokens: tokens})
}
policy.NativeRules = converted
return policy, nil
}
func dockerPolicyRuleKey(tokens []string, backend string) string {
tokens = nativeRuleTokens(tokens)
if backend == "iptables" {
if len(tokens)%2 != 0 {
return ""
}
protocol := ""
if index := slices.Index(tokens, "-p"); index >= 0 && index+1 < len(tokens) {
protocol = tokens[index+1]
}
parts := make([]string, 0, len(tokens)/2)
for i := 0; i < len(tokens); i += 2 {
option, value := tokens[i], tokens[i+1]
if option == "-m" && value == protocol && (value == "tcp" || value == "udp") {
continue
}
if option == "--ctorigdst" {
value = normalizeObservedHost(value)
}
if option == "-s" {
if address, err := netip.ParseAddr(value); err == nil {
value = netip.PrefixFrom(address, address.BitLen()).String()
} else if prefix, err := netip.ParsePrefix(value); err == nil {
value = prefix.Masked().String()
}
}
parts = append(parts, option+"\x00"+value)
}
sort.Strings(parts)
return strings.Join(parts, "\x01")
}
parts := make([]string, 0, len(tokens))
for i, token := range tokens {
if token == "counter" {
continue
}
if unquoted, err := strconv.Unquote(token); err == nil {
token = unquoted
}
if i > 0 && tokens[i-1] == "daddr" {
token = normalizeObservedHost(token)
}
if i > 0 && tokens[i-1] == "saddr" {
if address, err := netip.ParseAddr(token); err == nil {
token = netip.PrefixFrom(address, address.BitLen()).String()
} else if prefix, err := netip.ParsePrefix(token); err == nil {
token = prefix.Masked().String()
}
}
parts = append(parts, token)
}
return strings.Join(parts, "\x00")
}
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
@@ -35,33 +136,26 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
if err != nil {
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
}
managed := strings.Contains(line, "1panel-docker:")
if !managed && !hasAcceptAction(tokens) {
continue
}
sequence++
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil {
return PolicyInventory{}, err
}
identity := fragment.UUID
if action == "accept" {
identity = action
if action == "" || (fragment.HostPort == 0 && action == "return") {
continue
}
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
key := strings.Join([]string{fragment.UUID, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
group, exists := groups[key]
if !exists {
group = &observedPolicy{policy: fragment, sequence: sequence}
group = &observedPolicy{policy: fragment}
groups[key] = group
order = append(order, key)
}
switch {
case action == "accept" && source != "":
group.acceptedSource = append(group.acceptedSource, source)
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "accept":
group.acceptAll = true
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "":
@@ -71,21 +165,19 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
default:
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
if action != "accept" {
group.managedOrders = append(group.managedOrders, sequence)
}
group.policy.NativeRules = append(group.policy.NativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
group.orders = append(group.orders, sequence)
}
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), RuleOrders: make(map[string][]int64)}
for _, key := range order {
group := groups[key]
if group.acceptAll || len(group.acceptedSource) > 0 {
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
})
continue
}
switch {
case group.acceptAll:
group.policy.Mode = ModeAcceptAll
group.policy.Sources = []string{}
case len(group.acceptedSource) > 0:
group.policy.Mode = ModeAcceptSources
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
case len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
@@ -97,8 +189,16 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
if _, err := uuid.Parse(group.policy.UUID); err != nil {
rules := make([][]string, 0, len(group.policy.NativeRules))
for _, rule := range group.policy.NativeRules {
rules = append(rules, rule.Tokens)
}
fingerprint, _ := json.Marshal(rules)
group.policy.UUID = uuid.NewSHA1(uuid.NameSpaceOID, append([]byte(family+"\x00"), fingerprint...)).String()
}
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...)
inventory.RuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.orders...)
}
return inventory, nil
}
@@ -185,7 +285,10 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
action = tokens[index]
}
}
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
if action != "accept" && action != "drop" && action != "return" {
return policy, "", "", nil
}
if action == "drop" && (policy.Protocol == "" || policy.HostPort == 0) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
@@ -194,18 +297,6 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
return policy, source, action, nil
}
func hasAcceptAction(tokens []string) bool {
for index, token := range tokens {
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
return true
}
if strings.EqualFold(token, "accept") && !(index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment")) {
return true
}
}
return false
}
func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String()
@@ -241,3 +332,17 @@ func uniqueSortedStrings(values []string) []string {
sort.Strings(result)
return result
}
type orderedPolicyRule struct {
order int64
rule []string
}
func sortPolicyRules(segments []orderedPolicyRule) [][]string {
sort.SliceStable(segments, func(i, j int) bool { return segments[i].order < segments[j].order })
rules := make([][]string, 0, len(segments))
for _, segment := range segments {
rules = append(rules, segment.rule)
}
return rules
}
+23 -21
View File
@@ -18,6 +18,8 @@ const (
ModeSources = "deny_sources"
ModeAllow = "allow_sources"
ModeAll = "deny_all"
ModeAcceptSources = "accept_sources"
ModeAcceptAll = "accept_all"
StatusEffective = "effective"
StatusDisabled = "disabled"
StatusNotEffective = "not_effective"
@@ -35,7 +37,12 @@ type FamilyError struct {
Err error
}
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
func (e *FamilyError) Error() string {
if err, ok := e.Err.(buserr.BusinessError); ok && err.Msg == "ErrDockerForwardPolicyDrop" {
return err.Error()
}
return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err)
}
func (e *FamilyError) Unwrap() error { return e.Err }
type ProxyEndpoint struct {
@@ -55,16 +62,18 @@ type DNATRules struct {
}
type Policy struct {
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
NativeRules []NativeRule `json:",omitempty"`
}
type FamilyStatus struct {
Partial bool
State string
Reason string
Initialized bool
@@ -78,24 +87,17 @@ type NativeRule struct {
Tokens []string `json:"tokens"`
}
type ReadOnlyPolicy struct {
Policy Policy
Action string
Sequence int64
NativeRules []NativeRule
}
type PolicyInventory struct {
Policies []Policy
ReadOnly []ReadOnlyPolicy
ManagedRuleOrders map[string][]int64
Policies []Policy
RuleOrders map[string][]int64
}
type Runtime interface {
Initialize([]Policy, PolicyInventory) error
Bind() error
Initialize([]Policy, PolicyInventory, ...string) error
Bind(...string) error
OperateFamily(string, bool) error
ReplacePolicies([]Policy, PolicyInventory) error
Unbind() error
Unbind(...string) error
Cleanup() error
Initialized(string) (bool, error)
Status(string) FamilyStatus
+11 -11
View File
@@ -9,28 +9,29 @@ var (
ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable")
ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable")
ErrFamilyUnavailable = errors.New("firewall address family is unavailable")
ErrRuleNotFound = errors.New("firewall rule does not exist")
)
type ChangeOperation string
const (
ChangeCreate ChangeOperation = "create"
ChangeAdopt ChangeOperation = "adopt"
ChangeUpdate ChangeOperation = "update"
ChangeDelete ChangeOperation = "delete"
ChangeReorder ChangeOperation = "reorder"
)
type RuleChange struct {
CommandOnly bool `json:"-"`
UnmarkedAdopted bool `json:"-"`
Operation ChangeOperation `json:"operation"`
Before *FirewallRule `json:"before,omitempty"`
After *FirewallRule `json:"after,omitempty"`
Locator *Locator `json:"locator,omitempty"`
PreviousMarker string `json:"previousMarker,omitempty"`
Append bool `json:"append,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
Target *ObservedRule `json:"-"`
Raw string `json:"raw,omitempty"`
CommandOnly bool `json:"-"`
Operation ChangeOperation `json:"operation"`
Before *FirewallRule `json:"before,omitempty"`
After *FirewallRule `json:"after,omitempty"`
Locator *Locator `json:"locator,omitempty"`
PreviousMarker string `json:"previousMarker,omitempty"`
Append bool `json:"append,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
}
type NativeCommand struct {
@@ -69,7 +70,6 @@ func (p CommandBatch) CreatesOnly() bool {
type Adapter interface {
Provider() Provider
Capabilities(context.Context) (Capabilities, error)
ListRules(context.Context, Scope) (RuleSet, error)
BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
RunCommands(context.Context, CommandBatch) error
+27 -22
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
"strings"
)
@@ -53,28 +54,6 @@ func RuleMatchKey(rule FirewallRule) (string, error) {
return normalizedRuleKey(normalized)
}
func OppositeActions(left, right Action) bool {
return left == ActionAccept && (right == ActionDrop || right == ActionReject) ||
right == ActionAccept && (left == ActionDrop || left == ActionReject)
}
func SameRuleContent(before, after FirewallRule) (bool, error) {
before, err := NormalizeRule(before)
if err != nil {
return false, err
}
after, err = NormalizeRule(after)
if err != nil {
return false, err
}
previous, err := RuleMatchKey(before)
if err != nil {
return false, err
}
requested, err := RuleMatchKey(after)
return err == nil && previous == requested && before.Action == after.Action, err
}
func normalizedRuleKey(normalized FirewallRule) (string, error) {
identity := ruleIdentity{
Scope: normalized.Scope.Key(),
@@ -231,3 +210,29 @@ func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) b
wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey
}
var descriptionCounterPattern = regexp.MustCompile(`\bcounter packets \d+ bytes \d+\b`)
func DescriptionID(observed ObservedRule) (string, error) {
if observed.ParseStatus == ParseStatusSupported {
key, err := RuleKey(observed.Rule)
return "firewall:" + key, err
}
raw := observed.Raw
if observed.Rule.Scope.Provider == ProviderUFW {
if end := strings.Index(raw, "]"); strings.HasPrefix(strings.TrimSpace(raw), "[") && end >= 0 {
raw = strings.Join(strings.Fields(raw[end+1:]), " ")
}
}
if observed.Rule.Scope.Provider == ProviderNftables {
raw = descriptionCounterPattern.ReplaceAllString(raw, "counter")
}
payload, err := json.Marshal(struct {
Scope Scope
Raw string
}{observed.Rule.Scope.Normalize(), raw})
if err != nil {
return "", err
}
return fmt.Sprintf("firewall:%x", sha256.Sum256(payload)), nil
}
+4 -61
View File
@@ -1,70 +1,13 @@
package filter
import (
"github.com/1Panel-dev/1Panel/agent/constant"
)
type RuleOrigin string
const (
RuleOriginCreated RuleOrigin = constant.FirewallRuleOriginCreated
RuleOriginAdopted RuleOrigin = constant.FirewallRuleOriginAdopted
)
type InventoryState string
const (
InventoryStateManaged InventoryState = "managed"
InventoryStateAdopted InventoryState = "adopted"
InventoryStateExternal InventoryState = "external"
InventoryStateDrifted InventoryState = "drifted"
InventoryStateProtected InventoryState = "protected"
)
type InventoryMatch string
const (
InventoryMatchNone InventoryMatch = "none"
InventoryMatchExact InventoryMatch = "exact"
InventoryMatchChanged InventoryMatch = "changed"
InventoryMatchMissing InventoryMatch = "missing"
InventoryMatchAmbiguous InventoryMatch = "ambiguous"
InventoryMatchOpaque InventoryMatch = "opaque"
)
type DesiredRule struct {
UUID string `json:"uuid"`
Rule FirewallRule `json:"rule"`
RuleKey string `json:"ruleKey"`
Origin RuleOrigin `json:"origin"`
Protected bool `json:"protected,omitempty"`
Expanded bool `json:"expanded,omitempty"`
Marker string `json:"marker,omitempty"`
ObservedInstanceKey string `json:"observedInstanceKey,omitempty"`
}
type PositionRange struct {
Min int `json:"min"`
Max int `json:"max"`
}
type InventoryItem struct {
Incompatible bool `json:"incompatible,omitempty"`
Error string `json:"error,omitempty"`
Rule FirewallRule `json:"rule"`
Observed *ObservedRule `json:"observed,omitempty"`
Desired *DesiredRule `json:"desired,omitempty"`
State InventoryState `json:"state"`
Match InventoryMatch `json:"match"`
}
type Inventory struct {
Items []InventoryItem `json:"items"`
Notices []ScopeNotice `json:"notices,omitempty"`
}
type InventoryMergeInput struct {
Observed []ObservedRule
Desired []DesiredRule
ProtectedObservedKeys map[string]struct{}
Rule FirewallRule `json:"rule"`
Observed *ObservedRule `json:"observed"`
IsWhitelist bool `json:"isWhitelist"`
DescriptionID string `json:"descriptionID"`
}
-9
View File
@@ -88,9 +88,7 @@ const (
type ScopeNoticeCode string
const (
ScopeNoticeDefaultScopeMismatch ScopeNoticeCode = "default_scope_mismatch"
ScopeNoticeManagedScopeInactive ScopeNoticeCode = "managed_scope_inactive"
ScopeNoticeUnmanagedActiveScopes ScopeNoticeCode = "unmanaged_active_scopes"
ScopeNoticeRuntimePermanentMismatch ScopeNoticeCode = "runtime_permanent_mismatch"
ScopeNoticeManagedScopeMissing ScopeNoticeCode = "managed_scope_missing"
ScopeNoticeFamilyUnavailable ScopeNoticeCode = "family_unavailable"
@@ -303,10 +301,3 @@ type RuleSet struct {
Rules []ObservedRule `json:"rules"`
Notices []ScopeNotice `json:"notices,omitempty"`
}
type Capabilities struct {
Marker bool
OwnedChains bool
ExplicitPosition bool
ExplicitPriority bool
}

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